@friggframework/core 2.0.0--canary.704.79d9a63.0 → 2.0.0--canary.686.e94de4d.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 (207) hide show
  1. package/CLAUDE.md +28 -40
  2. package/README.md +105 -0
  3. package/application/commands/entity-commands.js +60 -18
  4. package/application/commands/integration-commands.js +20 -73
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/application/index.js +6 -2
  8. package/core/Worker.js +31 -7
  9. package/core/create-handler.js +9 -0
  10. package/credential/repositories/credential-repository-documentdb.js +34 -0
  11. package/credential/repositories/credential-repository-interface.js +15 -0
  12. package/credential/repositories/credential-repository-mongo.js +23 -0
  13. package/credential/repositories/credential-repository-postgres.js +28 -0
  14. package/credential/use-cases/credential-operations-for-user.js +202 -0
  15. package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
  16. package/database/config.js +48 -24
  17. package/database/documentdb-encryption-service.js +9 -22
  18. package/database/documentdb-utils.js +2 -0
  19. package/database/encryption/README.md +76 -14
  20. package/database/encryption/documentdb-encryption-service.md +78 -1
  21. package/database/encryption/encryption-config.js +219 -0
  22. package/database/encryption/encryption-schema-registry.js +93 -12
  23. package/database/encryption/integration-mapping-encryption.js +1 -1
  24. package/database/organization-where.js +18 -0
  25. package/database/prisma.js +15 -38
  26. package/database/unique-violation.js +25 -0
  27. package/database/use-cases/check-encryption-health-use-case.js +43 -52
  28. package/errors/index.js +0 -4
  29. package/extensions/bootstrap-runner.js +43 -0
  30. package/extensions/extension-loader.js +187 -0
  31. package/extensions/index.js +41 -0
  32. package/extensions/initialize-app.js +123 -0
  33. package/extensions/route-mounter.js +103 -0
  34. package/extensions/schema-composer.js +188 -0
  35. package/generated/prisma-mongodb/edge.js +43 -4
  36. package/generated/prisma-mongodb/index-browser.js +40 -1
  37. package/generated/prisma-mongodb/index.d.ts +4761 -133
  38. package/generated/prisma-mongodb/index.js +43 -4
  39. package/generated/prisma-mongodb/package.json +1 -1
  40. package/generated/prisma-mongodb/schema.prisma +92 -0
  41. package/generated/prisma-mongodb/wasm.js +43 -4
  42. package/generated/prisma-postgresql/edge.js +43 -4
  43. package/generated/prisma-postgresql/index-browser.js +40 -1
  44. package/generated/prisma-postgresql/index.d.ts +4961 -28
  45. package/generated/prisma-postgresql/index.js +43 -4
  46. package/generated/prisma-postgresql/package.json +1 -1
  47. package/generated/prisma-postgresql/schema.prisma +87 -0
  48. package/generated/prisma-postgresql/wasm.js +43 -4
  49. package/handlers/WEBHOOKS.md +0 -29
  50. package/handlers/app-definition-loader.js +121 -19
  51. package/handlers/app-handler-helpers.js +25 -3
  52. package/handlers/backend-utils.js +31 -25
  53. package/handlers/middleware/admin-auth.js +14 -1
  54. package/handlers/routers/auth.js +46 -10
  55. package/handlers/routers/health.js +13 -4
  56. package/handlers/routers/integration-defined-routers.js +144 -92
  57. package/handlers/routers/integration-webhook-routers.js +102 -64
  58. package/handlers/workers/integration-defined-workers.js +5 -2
  59. package/index.js +65 -3
  60. package/infrastructure/scheduler/index.js +19 -6
  61. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  62. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  63. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  64. package/integrations/index.js +1 -0
  65. package/integrations/integration-base.js +0 -29
  66. package/integrations/integration-router.js +131 -96
  67. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  68. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  71. package/integrations/repositories/integration-mapping-repository.js +0 -29
  72. package/integrations/repositories/integration-not-found.js +14 -0
  73. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  74. package/integrations/repositories/integration-repository-interface.js +17 -31
  75. package/integrations/repositories/integration-repository-mongo.js +60 -129
  76. package/integrations/repositories/integration-repository-postgres.js +62 -134
  77. package/integrations/repositories/process-repository-mongo.js +1 -1
  78. package/integrations/schedule-config.js +80 -0
  79. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  80. package/integrations/use-cases/create-integration.js +72 -75
  81. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  82. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  83. package/integrations/use-cases/index.js +7 -1
  84. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  85. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  86. package/integrations/utils/map-integration-dto.js +43 -14
  87. package/management-api/create-management-api-router.js +49 -0
  88. package/management-api/error-response.js +116 -0
  89. package/management-api/management-api-config.js +151 -0
  90. package/management-api/meta-router.js +76 -0
  91. package/management-api/openapi/build-openapi.js +144 -0
  92. package/management-api/openapi/components.js +408 -0
  93. package/management-api/openapi/v1-operations.js +83 -0
  94. package/management-api/openapi/v2-operations.js +264 -0
  95. package/management-api/route-registry.js +411 -0
  96. package/management-api/test-support/http-client.js +39 -0
  97. package/management-api/use-cases/get-management-api-meta.js +57 -0
  98. package/management-api/v1-deprecation.js +120 -0
  99. package/management-api/v2/create-v2-router.js +85 -0
  100. package/management-api/v2/dependencies.js +281 -0
  101. package/management-api/v2/dto.js +143 -0
  102. package/management-api/v2/handlers/authorization.js +94 -0
  103. package/management-api/v2/handlers/credentials.js +33 -0
  104. package/management-api/v2/handlers/entities.js +77 -0
  105. package/management-api/v2/handlers/integrations.js +128 -0
  106. package/management-api/v2/handlers/proxy.js +34 -0
  107. package/management-api/v2/request.js +85 -0
  108. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  109. package/modules/domain/authorization-session.js +144 -0
  110. package/modules/entity-scope.js +15 -0
  111. package/modules/entity-type-registry.js +311 -0
  112. package/modules/index.js +14 -0
  113. package/modules/module-factory.js +12 -0
  114. package/modules/module.js +53 -2
  115. package/modules/proxy/proxy-errors.js +20 -0
  116. package/modules/proxy/proxy-path.js +179 -0
  117. package/modules/proxy/proxy-policy.js +248 -0
  118. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  119. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  120. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  121. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  122. package/modules/repositories/entity-not-found.js +18 -0
  123. package/modules/repositories/module-repository-documentdb.js +51 -6
  124. package/modules/repositories/module-repository-interface.js +19 -2
  125. package/modules/repositories/module-repository-mongo.js +67 -74
  126. package/modules/repositories/module-repository-postgres.js +66 -74
  127. package/modules/repositories/module-repository.js +2 -1
  128. package/modules/requester/proxy-request.js +244 -0
  129. package/modules/requester/requester.js +27 -2
  130. package/modules/scope-config.js +192 -0
  131. package/modules/scope-policy.js +112 -0
  132. package/modules/scope-resolution.js +288 -0
  133. package/modules/scope-visibility.js +247 -0
  134. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  135. package/modules/tests/doubles/ownership-matching.js +18 -0
  136. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  137. package/modules/use-cases/create-entity-from-credential.js +113 -0
  138. package/modules/use-cases/entity-operations-for-user.js +162 -0
  139. package/modules/use-cases/entity-types.js +139 -0
  140. package/modules/use-cases/execute-entity-proxy-request.js +286 -0
  141. package/modules/use-cases/get-authorization-requirements.js +100 -0
  142. package/modules/use-cases/get-entities-for-user.js +26 -13
  143. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  144. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  145. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  146. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  147. package/modules/use-cases/get-module.js +10 -4
  148. package/modules/use-cases/manage-access.js +21 -0
  149. package/modules/use-cases/multi-step-authorization.js +329 -0
  150. package/modules/use-cases/process-authorization-callback.js +53 -19
  151. package/modules/use-cases/refresh-entity-options.js +10 -4
  152. package/modules/use-cases/test-module-auth.js +10 -4
  153. package/modules/utils/map-module-dto.js +3 -5
  154. package/package.json +6 -5
  155. package/prisma-mongodb/schema.prisma +92 -0
  156. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  157. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  158. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  159. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  160. package/prisma-postgresql/schema.prisma +87 -0
  161. package/providers/index.js +23 -0
  162. package/providers/resolve-provider.js +93 -0
  163. package/providers/runtime-detection.js +124 -0
  164. package/queues/index.js +25 -0
  165. package/queues/providers/index.js +11 -0
  166. package/queues/providers/netlify-background-provider.js +150 -0
  167. package/queues/providers/qstash-queue-provider.js +219 -0
  168. package/queues/providers/sqs-queue-provider.js +70 -0
  169. package/queues/queue-client-interface.js +55 -0
  170. package/queues/queue-message-auth.js +193 -0
  171. package/queues/queue-provider-factory.js +101 -0
  172. package/queues/queue-provider.js +65 -0
  173. package/queues/sqs-queue-client.js +81 -0
  174. package/types/index.d.ts +2 -0
  175. package/types/modules/index.d.ts +197 -0
  176. package/types/user/index.d.ts +134 -0
  177. package/user/adopter-jwt-config.js +177 -0
  178. package/user/adopter-jwt-errors.js +59 -0
  179. package/user/adopter-token-verifier.js +122 -0
  180. package/user/app-user-helpers.js +97 -0
  181. package/user/create-user-authentication.js +64 -0
  182. package/user/membership.js +51 -0
  183. package/user/repositories/organization-membership-record.js +19 -0
  184. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  185. package/user/repositories/organization-membership-repository-factory.js +42 -0
  186. package/user/repositories/organization-membership-repository-interface.js +57 -0
  187. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  188. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  189. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  190. package/user/repositories/user-repository-documentdb.js +27 -2
  191. package/user/repositories/user-repository-interface.js +18 -0
  192. package/user/repositories/user-repository-mongo.js +24 -0
  193. package/user/repositories/user-repository-postgres.js +18 -0
  194. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  195. package/user/tests/doubles/test-user-repository.js +1 -1
  196. package/user/use-cases/authenticate-user.js +7 -1
  197. package/user/use-cases/ensure-organization-membership.js +92 -0
  198. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  199. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  200. package/user/use-cases/get-user-from-bearer-token.js +2 -7
  201. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  202. package/user/user.js +64 -1
  203. package/errors/mapping-already-exists-error.js +0 -14
  204. package/integrations/use-cases/add-entities-to-integration.js +0 -92
  205. package/integrations/use-cases/remove-entities-from-integration.js +0 -70
  206. package/integrations/utils/assert-entity-ids-input.js +0 -42
  207. package/modules/utils/to-credential-id.js +0 -14
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Response shapes for Management API v2. Use cases return domain records;
3
+ * these functions decide what leaves the server. Credential material never
4
+ * does (see maskCredential).
5
+ */
6
+
7
+ const idOf = (value) => {
8
+ if (value === undefined || value === null) return null;
9
+ if (typeof value === 'object') {
10
+ const id = value.id ?? value._id;
11
+ return id === undefined || id === null ? null : String(id);
12
+ }
13
+ return String(value);
14
+ };
15
+
16
+ /**
17
+ * `{ scope, owner, organizationId, access }` (ADR-056 §3.4): whose a record is
18
+ * and what the caller may do with it. Empty when the use case did not say.
19
+ */
20
+ function tenancyFields(tenancy) {
21
+ if (!tenancy) return {};
22
+ return {
23
+ scope: tenancy.scope,
24
+ owner: tenancy.owner,
25
+ organizationId: tenancy.organizationId ?? null,
26
+ access: tenancy.access,
27
+ };
28
+ }
29
+
30
+ /** An entity (connected account). */
31
+ function toEntityDto(entity) {
32
+ const credential =
33
+ entity.credential && typeof entity.credential === 'object'
34
+ ? entity.credential
35
+ : null;
36
+ return {
37
+ id: String(entity.id),
38
+ type: entity.moduleName ?? null,
39
+ name: entity.name ?? null,
40
+ externalId: entity.externalId ?? null,
41
+ credentialId: idOf(entity.credential ?? entity.credentialId),
42
+ userId: idOf(entity.userId ?? entity.user),
43
+ authIsValid:
44
+ typeof credential?.authIsValid === 'boolean'
45
+ ? credential.authIsValid
46
+ : null,
47
+ ...tenancyFields(entity.tenancy),
48
+ };
49
+ }
50
+
51
+ /** An integration, from a hydrated instance or a stored DTO. */
52
+ function toIntegrationDto(integration) {
53
+ return {
54
+ id: String(integration.id),
55
+ entities: integration.entities ?? [],
56
+ status: integration.status ?? null,
57
+ config: integration.config ?? {},
58
+ version: integration.version ?? null,
59
+ messages: integration.messages ?? {},
60
+ ...tenancyFields(integration.tenancy),
61
+ };
62
+ }
63
+
64
+ // Fields every repository adds around the stored data; everything else on a
65
+ // credential record came from the provider and is treated as secret.
66
+ const CREDENTIAL_METADATA = new Set([
67
+ 'id',
68
+ '_id',
69
+ 'userId',
70
+ 'user',
71
+ 'externalId',
72
+ 'authIsValid',
73
+ 'createdAt',
74
+ 'updatedAt',
75
+ 'type',
76
+ 'entityIds',
77
+ 'status',
78
+ // Added by the credential use cases (ADR-056 §3.4); never provider data.
79
+ 'tenancy',
80
+ ]);
81
+
82
+ /**
83
+ * Masks one stored credential value. Strings keep at most their last four
84
+ * characters, and only when long enough that four characters reveal nothing
85
+ * useful; objects and arrays are hidden whole. Numbers, booleans and null
86
+ * (expiry times, flags) are not secret and pass through.
87
+ */
88
+ function maskValue(value) {
89
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
90
+ return value;
91
+ }
92
+ if (typeof value === 'string') {
93
+ return value.length >= 16 ? `****${value.slice(-4)}` : '****';
94
+ }
95
+ return '****';
96
+ }
97
+
98
+ const toIso = (value) => {
99
+ if (!value) return null;
100
+ const date = value instanceof Date ? value : new Date(value);
101
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
102
+ };
103
+
104
+ /**
105
+ * A credential with every provider field masked. The keys stay visible so a
106
+ * client can tell an OAuth credential from an API-key one; the values never
107
+ * leave the server.
108
+ */
109
+ function maskCredential(credential) {
110
+ const data = {};
111
+ for (const [key, value] of Object.entries(credential)) {
112
+ if (CREDENTIAL_METADATA.has(key) || value === undefined) continue;
113
+ data[key] = maskValue(value);
114
+ }
115
+ const entityIds = Array.isArray(credential.entityIds)
116
+ ? credential.entityIds.map(String)
117
+ : [];
118
+ return {
119
+ id: String(credential.id),
120
+ type: credential.type ?? null,
121
+ externalId: credential.externalId ?? null,
122
+ userId: idOf(credential.userId),
123
+ authIsValid:
124
+ typeof credential.authIsValid === 'boolean'
125
+ ? credential.authIsValid
126
+ : null,
127
+ entityIds,
128
+ entityCount: entityIds.length,
129
+ createdAt: toIso(credential.createdAt),
130
+ updatedAt: toIso(credential.updatedAt),
131
+ ...tenancyFields(credential.tenancy),
132
+ data,
133
+ };
134
+ }
135
+
136
+ module.exports = {
137
+ tenancyFields,
138
+ toEntityDto,
139
+ toIntegrationDto,
140
+ maskCredential,
141
+ maskValue,
142
+ idOf,
143
+ };
@@ -0,0 +1,94 @@
1
+ const { toEntityDto, maskCredential } = require('../dto');
2
+ const { objectBody, requireFields, parseStep, optionalString } = require('../request');
3
+
4
+ /** The v2 body for a submitted step: masked records when complete. */
5
+ function toAuthorizationResponse(result) {
6
+ if (result.status !== 'complete') return result;
7
+ return {
8
+ status: 'complete',
9
+ entity: result.entity ? toEntityDto(result.entity) : null,
10
+ credential: result.credential ? maskCredential(result.credential) : null,
11
+ ...(result.previousCredentialId && {
12
+ previousCredentialId: result.previousCredentialId,
13
+ }),
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Management API v2 authorization handlers (multi-step authorize and
19
+ * credential re-authorization), keyed by operationId.
20
+ */
21
+ function createAuthorizationHandlers(useCases) {
22
+ const {
23
+ getAuthorizationStep,
24
+ submitAuthorizationStep,
25
+ getCredentialReauthorization,
26
+ reauthorizeCredentialForUser,
27
+ } = useCases;
28
+
29
+ const stepQuery = (req) => ({
30
+ step: parseStep(req.query.step),
31
+ sessionId: optionalString(req.query.sessionId, 'sessionId'),
32
+ state: optionalString(req.query.state, 'state'),
33
+ });
34
+
35
+ const stepBody = (req) => {
36
+ const body = objectBody(req);
37
+ return {
38
+ body,
39
+ step: parseStep(body.step),
40
+ sessionId: optionalString(body.sessionId, 'sessionId'),
41
+ };
42
+ };
43
+
44
+ return {
45
+ async getAuthorizationRequirements(req, res, user) {
46
+ const entityType = optionalString(req.query.entityType, 'entityType');
47
+ requireFields({ entityType }, ['entityType']);
48
+ res.json(
49
+ await getAuthorizationStep.execute({
50
+ user,
51
+ entityType,
52
+ ...stepQuery(req),
53
+ })
54
+ );
55
+ },
56
+
57
+ async authorize(req, res, user) {
58
+ const { body, step, sessionId } = stepBody(req);
59
+ requireFields(body, ['entityType', 'data']);
60
+ const result = await submitAuthorizationStep.execute({
61
+ user,
62
+ entityType: optionalString(body.entityType, 'entityType'),
63
+ data: body.data,
64
+ step,
65
+ sessionId,
66
+ scope: optionalString(body.scope, 'scope'),
67
+ });
68
+ res.json(toAuthorizationResponse(result));
69
+ },
70
+
71
+ async getCredentialReauthorizeRequirements(req, res, user) {
72
+ res.json(
73
+ await getCredentialReauthorization.execute(
74
+ req.params.credentialId,
75
+ user,
76
+ stepQuery(req)
77
+ )
78
+ );
79
+ },
80
+
81
+ async reauthorizeCredential(req, res, user) {
82
+ const { body, step, sessionId } = stepBody(req);
83
+ requireFields(body, ['data']);
84
+ const result = await reauthorizeCredentialForUser.execute(
85
+ req.params.credentialId,
86
+ user,
87
+ { data: body.data, step, sessionId }
88
+ );
89
+ res.json(toAuthorizationResponse(result));
90
+ },
91
+ };
92
+ }
93
+
94
+ module.exports = { createAuthorizationHandlers, toAuthorizationResponse };
@@ -0,0 +1,33 @@
1
+ const { maskCredential } = require('../dto');
2
+ const { parseListFilters } = require('../request');
3
+
4
+ /** Management API v2 credential handlers, keyed by operationId. */
5
+ function createCredentialHandlers(useCases) {
6
+ const { listCredentialsForUser, getCredentialForUser, deleteCredentialForUser } =
7
+ useCases;
8
+
9
+ return {
10
+ async listCredentials(req, res, user) {
11
+ const credentials = await listCredentialsForUser.execute(
12
+ user,
13
+ parseListFilters(req.query)
14
+ );
15
+ res.json({ credentials: credentials.map(maskCredential) });
16
+ },
17
+
18
+ async getCredential(req, res, user) {
19
+ res.json(
20
+ maskCredential(
21
+ await getCredentialForUser.execute(req.params.credentialId, user)
22
+ )
23
+ );
24
+ },
25
+
26
+ async deleteCredential(req, res, user) {
27
+ await deleteCredentialForUser.execute(req.params.credentialId, user);
28
+ res.status(204).end();
29
+ },
30
+ };
31
+ }
32
+
33
+ module.exports = { createCredentialHandlers };
@@ -0,0 +1,77 @@
1
+ const { toEntityDto } = require('../dto');
2
+ const { objectBody, parseStep, parseListFilters } = 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(
21
+ user,
22
+ parseListFilters(req.query)
23
+ );
24
+ res.json({ entities: entities.map(toEntityDto) });
25
+ },
26
+
27
+ async listEntityTypes(_req, res, user) {
28
+ res.json({ types: await listEntityTypes.execute({ user }) });
29
+ },
30
+
31
+ async getEntityType(req, res, user) {
32
+ res.json(await getEntityType.execute(req.params.entityType, { user }));
33
+ },
34
+
35
+ // Describes a step's requirements without starting an authorization
36
+ // session; GET /api/v2/authorize starts one.
37
+ async getEntityTypeRequirements(req, res, user) {
38
+ res.json(
39
+ await getAuthorizationRequirements.execute({
40
+ entityType: req.params.entityType,
41
+ step: parseStep(req.query.step),
42
+ userId: user.getId(),
43
+ })
44
+ );
45
+ },
46
+
47
+ async getEntity(req, res, user) {
48
+ res.json(toEntityDto(await getEntityForUser.execute(req.params.entityId, user)));
49
+ },
50
+
51
+ async deleteEntity(req, res, user) {
52
+ await deleteEntityForUser.execute(req.params.entityId, user);
53
+ res.status(204).end();
54
+ },
55
+
56
+ async testEntityAuth(req, res, user) {
57
+ const ok = await testEntityAuthForUser.execute(req.params.entityId, user);
58
+ res.json({ status: ok ? 'ok' : 'failed' });
59
+ },
60
+
61
+ async getEntityOptions(req, res, user) {
62
+ res.json(await getEntityOptionsForUser.execute(req.params.entityId, user));
63
+ },
64
+
65
+ async refreshEntityOptions(req, res, user) {
66
+ res.json(
67
+ await refreshEntityOptionsForUser.execute(
68
+ req.params.entityId,
69
+ user,
70
+ objectBody(req)
71
+ )
72
+ );
73
+ },
74
+ };
75
+ }
76
+
77
+ module.exports = { createEntityHandlers };
@@ -0,0 +1,128 @@
1
+ const { toIntegrationDto } = require('../dto');
2
+ const { objectBody, optionalString, parseListFilters } = 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(
32
+ user,
33
+ parseListFilters(req.query)
34
+ );
35
+ res.json({ integrations: integrations.map(toIntegrationDto) });
36
+ },
37
+
38
+ async listIntegrationOptions(_req, res) {
39
+ res.json({ integrations: await getPossibleIntegrations.execute() });
40
+ },
41
+
42
+ async createIntegration(req, res, user) {
43
+ const { entities, config } = objectBody(req);
44
+ const integration = await createIntegrationForUser.execute(
45
+ { entities, config },
46
+ user
47
+ );
48
+ res.status(201).json(toIntegrationDto(integration));
49
+ },
50
+
51
+ async getIntegration(req, res, user) {
52
+ const instance = await getIntegrationInstanceForUser.execute(
53
+ req.params.integrationId,
54
+ user
55
+ );
56
+ res.json(toIntegrationDto(instance));
57
+ },
58
+
59
+ async updateIntegration(req, res, user) {
60
+ const { config } = objectBody(req);
61
+ const integration = await updateIntegrationForUser.execute(
62
+ req.params.integrationId,
63
+ user,
64
+ config
65
+ );
66
+ res.json(toIntegrationDto(integration));
67
+ },
68
+
69
+ async deleteIntegration(req, res, user) {
70
+ await removeIntegrationForUser.execute(req.params.integrationId, user);
71
+ res.status(204).end();
72
+ },
73
+
74
+ async getIntegrationConfigOptions(req, res, user) {
75
+ res.json(await send(req, user, 'GET_CONFIG_OPTIONS'));
76
+ },
77
+
78
+ async refreshIntegrationConfigOptions(req, res, user) {
79
+ res.json(await send(req, user, 'REFRESH_CONFIG_OPTIONS', objectBody(req)));
80
+ },
81
+
82
+ async listIntegrationActions(req, res, user) {
83
+ const actionType = optionalString(req.query.actionType, 'actionType');
84
+ res.json(
85
+ await send(req, user, 'GET_USER_ACTIONS', actionType ? { actionType } : {})
86
+ );
87
+ },
88
+
89
+ async getIntegrationActionOptions(req, res, user) {
90
+ res.json(
91
+ await send(req, user, 'GET_USER_ACTION_OPTIONS', {
92
+ actionId: req.params.actionId,
93
+ data: objectBody(req),
94
+ })
95
+ );
96
+ },
97
+
98
+ async refreshIntegrationActionOptions(req, res, user) {
99
+ res.json(
100
+ await send(req, user, 'REFRESH_USER_ACTION_OPTIONS', {
101
+ actionId: req.params.actionId,
102
+ data: objectBody(req),
103
+ })
104
+ );
105
+ },
106
+
107
+ async runIntegrationAction(req, res, user) {
108
+ res.json(
109
+ await runIntegrationActionForUser.execute(
110
+ req.params.integrationId,
111
+ user,
112
+ req.params.actionId,
113
+ objectBody(req)
114
+ )
115
+ );
116
+ },
117
+
118
+ async testIntegrationAuth(req, res, user) {
119
+ const { ok, errors } = await testIntegrationAuthForUser.execute(
120
+ req.params.integrationId,
121
+ user
122
+ );
123
+ res.json(ok ? { status: 'ok' } : { status: 'failed', errors });
124
+ },
125
+ };
126
+ }
127
+
128
+ module.exports = { createIntegrationHandlers };
@@ -0,0 +1,34 @@
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 };
@@ -0,0 +1,85 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { LIST_OWNERS } = require('../../modules/scope-visibility');
3
+
4
+ /** The JSON body as an object, or a 400 VALIDATION_ERROR. */
5
+ function objectBody(req) {
6
+ const body = req.body;
7
+ if (body === undefined || body === null || body === '') return {};
8
+ if (typeof body !== 'object' || Array.isArray(body)) {
9
+ throw Boom.badRequest('Request body must be a JSON object', {
10
+ code: 'VALIDATION_ERROR',
11
+ });
12
+ }
13
+ return body;
14
+ }
15
+
16
+ /** Fields that must be present (non-empty) in the body. */
17
+ function requireFields(body, fields) {
18
+ const missing = fields.filter(
19
+ (field) =>
20
+ body[field] === undefined || body[field] === null || body[field] === ''
21
+ );
22
+ if (missing.length) {
23
+ throw Boom.badRequest(
24
+ `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`,
25
+ { code: 'VALIDATION_ERROR', details: { missing } }
26
+ );
27
+ }
28
+ return body;
29
+ }
30
+
31
+ /** A 1-based step number from a query or body value; 1 when absent. */
32
+ function parseStep(value) {
33
+ if (value === undefined || value === null || value === '') return 1;
34
+ const step = Number(value);
35
+ if (!Number.isInteger(step) || step < 1) {
36
+ throw Boom.badRequest('step must be a positive integer', {
37
+ code: 'INVALID_STEP',
38
+ });
39
+ }
40
+ return step;
41
+ }
42
+
43
+ /** An optional string query/body value. */
44
+ function optionalString(value, name) {
45
+ if (value === undefined || value === null || value === '') return undefined;
46
+ if (typeof value !== 'string') {
47
+ throw Boom.badRequest(`${name} must be a string`, {
48
+ code: 'VALIDATION_ERROR',
49
+ details: { field: name },
50
+ });
51
+ }
52
+ return value;
53
+ }
54
+
55
+ const LIST_SCOPES = ['individual', 'organization'];
56
+
57
+ /**
58
+ * `?scope=individual|organization` and `?owner=me|org` on a list (ADR-056
59
+ * §3.4). Anything else is a 400.
60
+ */
61
+ function parseListFilters(query = {}) {
62
+ const scope = optionalString(query.scope, 'scope');
63
+ const owner = optionalString(query.owner, 'owner');
64
+ if (scope !== undefined && !LIST_SCOPES.includes(scope)) {
65
+ throw Boom.badRequest(`scope must be one of ${LIST_SCOPES.join(', ')}`, {
66
+ code: 'VALIDATION_ERROR',
67
+ details: { field: 'scope' },
68
+ });
69
+ }
70
+ if (owner !== undefined && !Object.hasOwn(LIST_OWNERS, owner)) {
71
+ throw Boom.badRequest('owner must be me or org', {
72
+ code: 'VALIDATION_ERROR',
73
+ details: { field: 'owner' },
74
+ });
75
+ }
76
+ return { scope, owner };
77
+ }
78
+
79
+ module.exports = {
80
+ objectBody,
81
+ requireFields,
82
+ parseStep,
83
+ optionalString,
84
+ parseListFilters,
85
+ };