@friggframework/core 2.0.0--canary.686.df38e8e.0 → 2.0.0--canary.710.9045bbc.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 (217) hide show
  1. package/CHANGELOG.md +0 -16
  2. package/CLAUDE.md +41 -29
  3. package/README.md +0 -105
  4. package/application/commands/README.md +17 -11
  5. package/application/commands/entity-commands.js +18 -60
  6. package/application/commands/integration-commands.js +73 -20
  7. package/application/commands/scheduler-commands.js +7 -11
  8. package/application/commands/user-commands.js +5 -17
  9. package/application/index.js +2 -6
  10. package/core/Worker.js +7 -31
  11. package/core/create-handler.js +0 -9
  12. package/credential/repositories/credential-repository-documentdb.js +0 -34
  13. package/credential/repositories/credential-repository-interface.js +0 -15
  14. package/credential/repositories/credential-repository-mongo.js +0 -23
  15. package/credential/repositories/credential-repository-postgres.js +0 -28
  16. package/database/config.js +24 -48
  17. package/database/documentdb-encryption-service.js +22 -9
  18. package/database/documentdb-utils.js +0 -2
  19. package/database/encryption/README.md +17 -81
  20. package/database/encryption/documentdb-encryption-service.md +1 -78
  21. package/database/encryption/encryption-schema-registry.js +46 -123
  22. package/database/encryption/field-encryption-service.js +5 -69
  23. package/database/encryption/integration-mapping-encryption.js +1 -1
  24. package/database/prisma.js +38 -15
  25. package/database/use-cases/check-encryption-health-use-case.js +52 -43
  26. package/errors/index.js +4 -0
  27. package/errors/mapping-already-exists-error.js +14 -0
  28. package/generated/prisma-mongodb/edge.js +4 -43
  29. package/generated/prisma-mongodb/index-browser.js +1 -40
  30. package/generated/prisma-mongodb/index.d.ts +111 -4739
  31. package/generated/prisma-mongodb/index.js +4 -43
  32. package/generated/prisma-mongodb/package.json +1 -1
  33. package/generated/prisma-mongodb/schema.prisma +0 -92
  34. package/generated/prisma-mongodb/wasm.js +4 -43
  35. package/generated/prisma-postgresql/edge.js +4 -43
  36. package/generated/prisma-postgresql/index-browser.js +1 -40
  37. package/generated/prisma-postgresql/index.d.ts +33 -4966
  38. package/generated/prisma-postgresql/index.js +4 -43
  39. package/generated/prisma-postgresql/package.json +1 -1
  40. package/generated/prisma-postgresql/schema.prisma +0 -87
  41. package/generated/prisma-postgresql/wasm.js +4 -43
  42. package/handlers/WEBHOOKS.md +29 -0
  43. package/handlers/app-definition-loader.js +19 -121
  44. package/handlers/app-handler-helpers.js +3 -25
  45. package/handlers/backend-utils.js +25 -31
  46. package/handlers/middleware/admin-auth.js +1 -14
  47. package/handlers/routers/auth.js +10 -46
  48. package/handlers/routers/health.js +4 -13
  49. package/handlers/routers/integration-defined-routers.js +92 -144
  50. package/handlers/routers/integration-webhook-routers.js +64 -102
  51. package/handlers/workers/integration-defined-workers.js +2 -5
  52. package/index.js +3 -65
  53. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +67 -13
  54. package/infrastructure/scheduler/index.js +6 -19
  55. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  56. package/integrations/index.js +0 -1
  57. package/integrations/integration-base.js +29 -0
  58. package/integrations/integration-router.js +96 -131
  59. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  60. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  61. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  62. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  63. package/integrations/repositories/integration-mapping-repository.js +29 -0
  64. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  65. package/integrations/repositories/integration-repository-interface.js +31 -17
  66. package/integrations/repositories/integration-repository-mongo.js +129 -60
  67. package/integrations/repositories/integration-repository-postgres.js +134 -62
  68. package/integrations/repositories/process-repository-mongo.js +1 -1
  69. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  70. package/integrations/use-cases/add-entities-to-integration.js +92 -0
  71. package/integrations/use-cases/create-integration.js +75 -72
  72. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  73. package/integrations/use-cases/index.js +1 -7
  74. package/integrations/use-cases/remove-entities-from-integration.js +70 -0
  75. package/integrations/utils/assert-entity-ids-input.js +42 -0
  76. package/integrations/utils/map-integration-dto.js +14 -43
  77. package/jest-setup.js +13 -2
  78. package/jest-teardown.js +5 -2
  79. package/jest.config.js +4 -0
  80. package/jest.db.config.js +23 -0
  81. package/modules/index.js +0 -14
  82. package/modules/module-factory.js +0 -12
  83. package/modules/module.js +2 -53
  84. package/modules/repositories/module-repository-documentdb.js +6 -51
  85. package/modules/repositories/module-repository-interface.js +2 -19
  86. package/modules/repositories/module-repository-mongo.js +74 -67
  87. package/modules/repositories/module-repository-postgres.js +74 -66
  88. package/modules/repositories/module-repository.js +1 -2
  89. package/modules/requester/requester.js +2 -27
  90. package/modules/use-cases/get-entities-for-user.js +13 -26
  91. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  92. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  93. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  94. package/modules/use-cases/get-module.js +4 -10
  95. package/modules/use-cases/process-authorization-callback.js +19 -53
  96. package/modules/use-cases/refresh-entity-options.js +4 -10
  97. package/modules/use-cases/test-module-auth.js +4 -10
  98. package/modules/utils/map-module-dto.js +5 -3
  99. package/modules/utils/to-credential-id.js +14 -0
  100. package/package.json +7 -6
  101. package/prisma-mongodb/schema.prisma +0 -92
  102. package/prisma-postgresql/schema.prisma +0 -87
  103. package/queues/index.js +0 -25
  104. package/queues/queuer-util.js +6 -5
  105. package/types/index.d.ts +0 -2
  106. package/user/repositories/user-repository-documentdb.js +2 -27
  107. package/user/repositories/user-repository-interface.js +0 -18
  108. package/user/repositories/user-repository-mongo.js +0 -24
  109. package/user/repositories/user-repository-postgres.js +0 -18
  110. package/user/tests/doubles/test-user-repository.js +1 -1
  111. package/user/use-cases/authenticate-user.js +1 -7
  112. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  113. package/user/use-cases/get-user-from-bearer-token.js +7 -2
  114. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  115. package/user/user.js +1 -64
  116. package/credential/use-cases/credential-operations-for-user.js +0 -202
  117. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  118. package/database/encryption/encryption-config.js +0 -219
  119. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  120. package/database/organization-where.js +0 -18
  121. package/database/unique-violation.js +0 -25
  122. package/extensions/bootstrap-runner.js +0 -43
  123. package/extensions/extension-loader.js +0 -187
  124. package/extensions/index.js +0 -41
  125. package/extensions/initialize-app.js +0 -123
  126. package/extensions/route-mounter.js +0 -103
  127. package/extensions/schema-composer.js +0 -188
  128. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -220
  129. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  130. package/integrations/repositories/integration-not-found.js +0 -14
  131. package/integrations/schedule-config.js +0 -80
  132. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  133. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  134. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  135. package/management-api/create-management-api-router.js +0 -49
  136. package/management-api/error-response.js +0 -116
  137. package/management-api/management-api-config.js +0 -151
  138. package/management-api/meta-router.js +0 -76
  139. package/management-api/openapi/build-openapi.js +0 -144
  140. package/management-api/openapi/components.js +0 -408
  141. package/management-api/openapi/v1-operations.js +0 -83
  142. package/management-api/openapi/v2-operations.js +0 -264
  143. package/management-api/route-registry.js +0 -411
  144. package/management-api/test-support/http-client.js +0 -39
  145. package/management-api/use-cases/get-management-api-meta.js +0 -57
  146. package/management-api/v1-deprecation.js +0 -120
  147. package/management-api/v2/create-v2-router.js +0 -85
  148. package/management-api/v2/dependencies.js +0 -281
  149. package/management-api/v2/dto.js +0 -143
  150. package/management-api/v2/handlers/authorization.js +0 -94
  151. package/management-api/v2/handlers/credentials.js +0 -33
  152. package/management-api/v2/handlers/entities.js +0 -77
  153. package/management-api/v2/handlers/integrations.js +0 -128
  154. package/management-api/v2/handlers/proxy.js +0 -34
  155. package/management-api/v2/request.js +0 -85
  156. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  157. package/modules/domain/authorization-session.js +0 -144
  158. package/modules/entity-scope.js +0 -15
  159. package/modules/entity-type-registry.js +0 -311
  160. package/modules/proxy/proxy-errors.js +0 -20
  161. package/modules/proxy/proxy-path.js +0 -179
  162. package/modules/proxy/proxy-policy.js +0 -248
  163. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  164. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  165. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  166. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  167. package/modules/repositories/entity-not-found.js +0 -18
  168. package/modules/requester/proxy-request.js +0 -244
  169. package/modules/scope-config.js +0 -192
  170. package/modules/scope-policy.js +0 -112
  171. package/modules/scope-resolution.js +0 -288
  172. package/modules/scope-visibility.js +0 -247
  173. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  174. package/modules/tests/doubles/ownership-matching.js +0 -18
  175. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  176. package/modules/use-cases/create-entity-from-credential.js +0 -113
  177. package/modules/use-cases/entity-operations-for-user.js +0 -162
  178. package/modules/use-cases/entity-types.js +0 -139
  179. package/modules/use-cases/execute-entity-proxy-request.js +0 -286
  180. package/modules/use-cases/get-authorization-requirements.js +0 -100
  181. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  182. package/modules/use-cases/manage-access.js +0 -21
  183. package/modules/use-cases/multi-step-authorization.js +0 -329
  184. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  185. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  186. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  187. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  188. package/providers/index.js +0 -23
  189. package/providers/resolve-provider.js +0 -93
  190. package/providers/runtime-detection.js +0 -124
  191. package/queues/providers/index.js +0 -11
  192. package/queues/providers/netlify-background-provider.js +0 -187
  193. package/queues/providers/qstash-queue-provider.js +0 -219
  194. package/queues/providers/sqs-queue-provider.js +0 -70
  195. package/queues/queue-client-interface.js +0 -55
  196. package/queues/queue-message-auth.js +0 -193
  197. package/queues/queue-provider-factory.js +0 -101
  198. package/queues/queue-provider.js +0 -65
  199. package/queues/sqs-queue-client.js +0 -81
  200. package/types/modules/index.d.ts +0 -197
  201. package/types/user/index.d.ts +0 -134
  202. package/user/adopter-jwt-config.js +0 -177
  203. package/user/adopter-jwt-errors.js +0 -59
  204. package/user/adopter-token-verifier.js +0 -122
  205. package/user/app-user-helpers.js +0 -97
  206. package/user/create-user-authentication.js +0 -64
  207. package/user/membership.js +0 -51
  208. package/user/repositories/organization-membership-record.js +0 -19
  209. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  210. package/user/repositories/organization-membership-repository-factory.js +0 -42
  211. package/user/repositories/organization-membership-repository-interface.js +0 -57
  212. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  213. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  214. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  215. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  216. package/user/use-cases/ensure-organization-membership.js +0 -92
  217. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,179 +0,0 @@
1
- const { ProxyRequestError } = require('./proxy-errors');
2
-
3
- /**
4
- * Path rules for the entity proxy (ADR-052 §4). The caller supplies a path
5
- * relative to the module's base URL and nothing else: the host, scheme and
6
- * port always come from the module.
7
- */
8
-
9
- const invalid = (reason) =>
10
- new ProxyRequestError('INVALID_PROXY_REQUEST', `Invalid proxy path: ${reason}`, {
11
- field: 'path',
12
- });
13
-
14
- // Control characters (C0, DEL, C1), any whitespace, and invisible format
15
- // characters such as zero-width spaces and byte-order marks.
16
- const FORBIDDEN_CHARS = /[\x00-\x20\x7f-\x9f]|\s|\p{Cf}/u;
17
-
18
- /**
19
- * Validates and normalises a caller-supplied path.
20
- *
21
- * - must start with a single `/`; `//` (anywhere), `\`, `?`, `#`, control
22
- * and whitespace characters are rejected;
23
- * - encoded separators (`%2f`, `%5c`) are rejected, then the path is
24
- * percent-decoded exactly once; a `%` left after decoding (double
25
- * encoding) is rejected, as is any decoded control or whitespace
26
- * character other than a space (`%20`);
27
- * - `.` and `..` segments are rejected rather than resolved.
28
- *
29
- * @param {unknown} path
30
- * @returns {string} the decoded path, e.g. `/crm/v3/objects/contacts`
31
- * @throws {ProxyRequestError} INVALID_PROXY_REQUEST
32
- */
33
- function normalizeProxyPath(path) {
34
- if (typeof path !== 'string' || path.length === 0) {
35
- throw invalid('path must be a non-empty string');
36
- }
37
- if (path.length > 2048) throw invalid('path is too long');
38
- if (!path.startsWith('/')) throw invalid('path must start with /');
39
- if (path.startsWith('//') || path.includes('//')) {
40
- throw invalid('empty path segments (//) are not allowed');
41
- }
42
- if (path.includes('\\')) throw invalid('backslashes are not allowed');
43
- if (path.includes('?') || path.includes('#')) {
44
- throw invalid('send the query string in `query`, not in the path');
45
- }
46
- if (FORBIDDEN_CHARS.test(path)) {
47
- throw invalid('control and whitespace characters are not allowed');
48
- }
49
- if (/%(2f|5c)/i.test(path)) {
50
- throw invalid('encoded path separators are not allowed');
51
- }
52
-
53
- let decoded;
54
- try {
55
- decoded = decodeURIComponent(path);
56
- } catch {
57
- throw invalid('malformed percent-encoding');
58
- }
59
- if (
60
- decoded.includes('%') ||
61
- decoded.includes('\\') ||
62
- decoded.includes('?') ||
63
- decoded.includes('#') ||
64
- decoded.includes('//') ||
65
- // An encoded space (%20) is a legitimate path character; every other
66
- // control or whitespace character stays forbidden.
67
- FORBIDDEN_CHARS.test(decoded.replaceAll(' ', ''))
68
- ) {
69
- throw invalid('encoded characters decode to something not allowed');
70
- }
71
-
72
- const segments = decoded.split('/').slice(1);
73
- if (segments.some((segment) => segment === '.' || segment === '..')) {
74
- throw invalid('dot segments (. and ..) are not allowed');
75
- }
76
- return decoded;
77
- }
78
-
79
- /**
80
- * Joins a module base URL and a normalised path, then proves the result
81
- * cannot leave the module's API: https only, same origin as the base URL,
82
- * and under the base URL's path.
83
- *
84
- * @param {string} baseUrl - from the module, never from the request
85
- * @param {string} normalizedPath - from normalizeProxyPath()
86
- * @returns {{ url: string, basePath: string, origin: string }} `url` is not
87
- * percent-encoded; the Requester encodes it once.
88
- * @throws {ProxyRequestError} PROXY_NOT_CONFIGURED or INVALID_PROXY_REQUEST
89
- */
90
- function joinProxyUrl(baseUrl, normalizedPath) {
91
- let base;
92
- try {
93
- base = new URL(baseUrl);
94
- } catch {
95
- throw new ProxyRequestError(
96
- 'PROXY_NOT_CONFIGURED',
97
- 'The module has no valid base URL to proxy to'
98
- );
99
- }
100
- if (base.protocol !== 'https:') {
101
- throw new ProxyRequestError(
102
- 'PROXY_NOT_CONFIGURED',
103
- 'The module base URL must use https to be proxied'
104
- );
105
- }
106
- if (base.username || base.password || base.search || base.hash) {
107
- throw new ProxyRequestError(
108
- 'PROXY_NOT_CONFIGURED',
109
- 'The module base URL must not carry credentials, a query or a fragment'
110
- );
111
- }
112
-
113
- const basePath = base.pathname.replace(/\/+$/, '');
114
- const url = `${base.origin}${basePath}${normalizedPath}`;
115
-
116
- let joined;
117
- try {
118
- joined = new URL(url);
119
- } catch {
120
- throw invalid('the joined URL is not valid');
121
- }
122
- const joinedPath = decodeURIComponent(joined.pathname);
123
- if (
124
- joined.protocol !== 'https:' ||
125
- joined.origin !== base.origin ||
126
- joined.username ||
127
- joined.password ||
128
- !(joinedPath === basePath || joinedPath.startsWith(`${basePath}/`))
129
- ) {
130
- throw invalid('the path leaves the module API');
131
- }
132
- return { url, basePath, origin: base.origin };
133
- }
134
-
135
- /**
136
- * Finds the allow-list rule that permits `method` on `path`.
137
- *
138
- * Patterns use `:param` for exactly one segment and a trailing `/**` for a
139
- * subtree (the prefix itself included). `method: '*'` matches any method.
140
- *
141
- * @param {Array<{method: string, path: string}>} rules
142
- * @param {string} method - upper case
143
- * @param {string} path - normalised path
144
- * @returns {{method: string, path: string}|null}
145
- */
146
- function matchAllowRule(rules, method, path) {
147
- const requestSegments = path.split('/').slice(1);
148
- for (const rule of rules || []) {
149
- if (!rule || typeof rule.path !== 'string' || typeof rule.method !== 'string') {
150
- continue;
151
- }
152
- const ruleMethod = rule.method.toUpperCase();
153
- if (ruleMethod !== '*' && ruleMethod !== method) continue;
154
- if (patternMatches(rule.path, requestSegments)) return rule;
155
- }
156
- return null;
157
- }
158
-
159
- function patternMatches(pattern, requestSegments) {
160
- if (!pattern.startsWith('/')) return false;
161
- let patternSegments = pattern.split('/').slice(1);
162
- let subtree = false;
163
- if (patternSegments[patternSegments.length - 1] === '**') {
164
- subtree = true;
165
- patternSegments = patternSegments.slice(0, -1);
166
- }
167
- if (subtree) {
168
- if (requestSegments.length < patternSegments.length) return false;
169
- } else if (requestSegments.length !== patternSegments.length) {
170
- return false;
171
- }
172
- return patternSegments.every((segment, index) => {
173
- const actual = requestSegments[index];
174
- if (segment.startsWith(':')) return actual !== undefined && actual !== '';
175
- return segment === actual;
176
- });
177
- }
178
-
179
- module.exports = { normalizeProxyPath, joinProxyUrl, matchAllowRule };
@@ -1,248 +0,0 @@
1
- const { ProxyRequestError } = require('./proxy-errors');
2
-
3
- /**
4
- * What the entity proxy accepts and returns (ADR-052 §5-§7). Pure functions:
5
- * no I/O, so every rule is unit-tested on its own.
6
- */
7
-
8
- const PROXY_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
9
- const METHODS_WITH_BODY = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
10
-
11
- /** Request headers a caller may always send (lower case). */
12
- const ALLOWED_REQUEST_HEADERS = [
13
- 'accept',
14
- 'content-type',
15
- 'accept-language',
16
- 'if-match',
17
- 'if-none-match',
18
- ];
19
-
20
- /** Request headers that are refused outright, even if a module lists them. */
21
- const REJECTED_REQUEST_HEADERS = [
22
- 'authorization',
23
- 'proxy-authorization',
24
- 'cookie',
25
- 'host',
26
- 'forwarded',
27
- 'x-real-ip',
28
- 'x-api-key',
29
- ];
30
- const REJECTED_REQUEST_HEADER_PREFIXES = ['x-forwarded-'];
31
-
32
- /** Response headers never passed back (credentials and hop-by-hop). */
33
- const REMOVED_RESPONSE_HEADERS = [
34
- 'authorization',
35
- 'proxy-authorization',
36
- 'proxy-authenticate',
37
- 'set-cookie',
38
- 'set-cookie2',
39
- 'www-authenticate',
40
- 'x-api-key',
41
- 'connection',
42
- 'keep-alive',
43
- 'te',
44
- 'trailer',
45
- 'transfer-encoding',
46
- 'upgrade',
47
- // The body is re-encoded as JSON, so these no longer describe it.
48
- 'content-length',
49
- 'content-encoding',
50
- ];
51
-
52
- const invalid = (message, details) =>
53
- new ProxyRequestError('INVALID_PROXY_REQUEST', message, details);
54
-
55
- const isPlainObject = (value) =>
56
- value !== null && typeof value === 'object' && !Array.isArray(value);
57
-
58
- /**
59
- * Validates the request body of POST /api/v2/entities/:id/proxy and returns
60
- * it with the method upper-cased and the body serialised.
61
- *
62
- * @param {unknown} request
63
- * @param {{ maxRequestBytes: number }} limits
64
- * @returns {{ method: string, path: string, query?: Object, headers: Object, body?: string, requestBytes: number, bodyIsJson: boolean }}
65
- */
66
- function validateProxyRequest(request, { maxRequestBytes }) {
67
- if (!isPlainObject(request)) throw invalid('The proxy request must be a JSON object');
68
- const method = typeof request.method === 'string' ? request.method.toUpperCase() : '';
69
- if (!PROXY_METHODS.includes(method)) {
70
- throw invalid(`method must be one of ${PROXY_METHODS.join(', ')}`, { field: 'method' });
71
- }
72
- if (typeof request.path !== 'string') {
73
- throw invalid('path is required', { field: 'path' });
74
- }
75
-
76
- let query;
77
- if (request.query !== undefined && request.query !== null) {
78
- if (!isPlainObject(request.query)) throw invalid('query must be an object', { field: 'query' });
79
- query = {};
80
- for (const [key, value] of Object.entries(request.query)) {
81
- const scalar = ['string', 'number', 'boolean'].includes(typeof value);
82
- const list = Array.isArray(value) && value.every((v) => typeof v === 'string');
83
- if (!scalar && !list) {
84
- throw invalid(`query.${key} must be a string, number, boolean or array of strings`, {
85
- field: `query.${key}`,
86
- });
87
- }
88
- // The requester sends one value per key; arrays go comma-separated.
89
- query[key] = list ? value.join(',') : value;
90
- }
91
- }
92
-
93
- const headers = {};
94
- if (request.headers !== undefined && request.headers !== null) {
95
- if (!isPlainObject(request.headers)) throw invalid('headers must be an object', { field: 'headers' });
96
- for (const [name, value] of Object.entries(request.headers)) {
97
- if (typeof value !== 'string') {
98
- throw invalid(`header ${name} must be a string`, { field: `headers.${name}` });
99
- }
100
- headers[name.toLowerCase()] = value;
101
- }
102
- }
103
-
104
- let body;
105
- let bodyIsJson = false;
106
- if (request.body !== undefined && request.body !== null) {
107
- if (!METHODS_WITH_BODY.has(method)) {
108
- throw invalid(`${method} requests cannot have a body`, { field: 'body' });
109
- }
110
- if (typeof request.body === 'string') {
111
- body = request.body;
112
- } else {
113
- body = JSON.stringify(request.body);
114
- bodyIsJson = true;
115
- }
116
- }
117
- const requestBytes = body === undefined ? 0 : Buffer.byteLength(body);
118
- if (requestBytes > maxRequestBytes) {
119
- throw new ProxyRequestError(
120
- 'PAYLOAD_TOO_LARGE',
121
- `The proxied request body is larger than ${maxRequestBytes} bytes`
122
- );
123
- }
124
- return { method, path: request.path, query, headers, body, bodyIsJson, requestBytes };
125
- }
126
-
127
- const isWildcardRule = (rule) => rule?.method === '*' || rule?.path === '/**';
128
-
129
- const isValidRule = (rule) =>
130
- isPlainObject(rule) &&
131
- typeof rule.method === 'string' &&
132
- typeof rule.path === 'string' &&
133
- rule.path.startsWith('/');
134
-
135
- /**
136
- * The allow-list in force for a module: the app definition's list for it when
137
- * one is given (`managementApi.proxy.modules[name].allow`), otherwise the
138
- * module's own `proxy.allow`. A module cannot grant itself a wildcard; only
139
- * the app definition can. No list means nothing may be proxied.
140
- *
141
- * @returns {{ rules: Array<{method: string, path: string}>, source: 'app'|'module'|'none', ignored: number }}
142
- */
143
- function resolveAllowRules(definition, moduleOverrides = {}) {
144
- const override = moduleOverrides?.[definition.moduleName]?.allow;
145
- if (Array.isArray(override)) {
146
- return { rules: override.filter(isValidRule), source: 'app', ignored: 0 };
147
- }
148
- const declared = Array.isArray(definition.proxy?.allow) ? definition.proxy.allow : [];
149
- const rules = declared.filter((rule) => isValidRule(rule) && !isWildcardRule(rule));
150
- return {
151
- rules,
152
- source: rules.length ? 'module' : 'none',
153
- ignored: declared.length - rules.length,
154
- };
155
- }
156
-
157
- /**
158
- * Keeps only the request headers the caller may set. A header from the
159
- * rejected list (credentials, Host, forwarding headers, or one the module's
160
- * own auth sets) fails the request instead of being dropped quietly.
161
- *
162
- * @param {Object} headers - lower-cased names
163
- * @param {Object} options
164
- * @param {string[]} [options.moduleHeaders] - from the module's `proxy.headers`
165
- * @param {string[]} [options.authHeaderNames] - headers the module's auth sets
166
- */
167
- function filterRequestHeaders(headers, { moduleHeaders = [], authHeaderNames = [] } = {}) {
168
- const rejected = new Set([
169
- ...REJECTED_REQUEST_HEADERS,
170
- ...authHeaderNames.filter(Boolean).map((name) => String(name).toLowerCase()),
171
- ]);
172
- const allowed = new Set([
173
- ...ALLOWED_REQUEST_HEADERS,
174
- ...moduleHeaders.map((name) => String(name).toLowerCase()),
175
- ]);
176
- const kept = {};
177
- for (const [name, value] of Object.entries(headers)) {
178
- if (
179
- rejected.has(name) ||
180
- REJECTED_REQUEST_HEADER_PREFIXES.some((prefix) => name.startsWith(prefix))
181
- ) {
182
- throw invalid(`The ${name} header cannot be sent through the proxy`, {
183
- field: `headers.${name}`,
184
- });
185
- }
186
- if (allowed.has(name)) kept[name] = value;
187
- }
188
- return kept;
189
- }
190
-
191
- /**
192
- * Removes credential and hop-by-hop headers from an upstream response, and
193
- * the Location of a redirect the proxy refused to follow.
194
- */
195
- function filterResponseHeaders(headers, { redirectBlocked = false, authHeaderNames = [] } = {}) {
196
- const removed = new Set([
197
- ...REMOVED_RESPONSE_HEADERS,
198
- ...authHeaderNames.filter(Boolean).map((name) => String(name).toLowerCase()),
199
- ]);
200
- if (redirectBlocked) removed.add('location');
201
- const kept = {};
202
- for (const [name, value] of Object.entries(headers || {})) {
203
- if (!removed.has(name.toLowerCase())) kept[name.toLowerCase()] = value;
204
- }
205
- return kept;
206
- }
207
-
208
- const JSON_TYPE = /^application\/([a-z0-9.+-]*\+)?json\b/i;
209
- const TEXT_TYPE = /^(text\/|application\/(xml|x-www-form-urlencoded|javascript|graphql)\b|[a-z]+\/[a-z0-9.+-]*\+xml\b)/i;
210
-
211
- /**
212
- * Turns the upstream body into the `data` of the proxy response: parsed JSON,
213
- * text, or (only if the module allows binary) base64.
214
- *
215
- * @returns {{ data: unknown, encoding?: 'base64' }}
216
- */
217
- function decodeResponseBody(body, contentType, { binary = false } = {}) {
218
- if (!body || body.length === 0) return { data: null };
219
- if (JSON_TYPE.test(contentType)) {
220
- const text = body.toString('utf8');
221
- try {
222
- return { data: JSON.parse(text) };
223
- } catch {
224
- return { data: text };
225
- }
226
- }
227
- if (!contentType || TEXT_TYPE.test(contentType)) {
228
- return { data: body.toString('utf8') };
229
- }
230
- if (binary) return { data: body.toString('base64'), encoding: 'base64' };
231
- throw new ProxyRequestError(
232
- 'UNSUPPORTED_MEDIA_TYPE',
233
- 'The upstream response is binary and this module does not allow binary proxying',
234
- { contentType: contentType.split(';')[0] }
235
- );
236
- }
237
-
238
- module.exports = {
239
- PROXY_METHODS,
240
- ALLOWED_REQUEST_HEADERS,
241
- REJECTED_REQUEST_HEADERS,
242
- REMOVED_RESPONSE_HEADERS,
243
- validateProxyRequest,
244
- resolveAllowRules,
245
- filterRequestHeaders,
246
- filterResponseHeaders,
247
- decodeResponseBody,
248
- };
@@ -1,112 +0,0 @@
1
- const { prisma } = require('../../database/prisma');
2
- const {
3
- findOne,
4
- insertOne,
5
- updateOne,
6
- deleteMany,
7
- } = require('../../database/documentdb-utils');
8
- const {
9
- DocumentDBEncryptionService,
10
- } = require('../../database/documentdb-encryption-service');
11
- const { AuthorizationSession } = require('../domain/authorization-session');
12
- const {
13
- AuthorizationSessionRepositoryInterface,
14
- } = require('./authorization-session-repository-interface');
15
-
16
- const COLLECTION = 'AuthorizationSession';
17
-
18
- /**
19
- * AuthorizationSession persistence for DocumentDB through raw commands.
20
- * `stepData` is encrypted with DocumentDBEncryptionService, as the Prisma
21
- * extension does not run for raw commands.
22
- */
23
- class AuthorizationSessionRepositoryDocumentDB extends AuthorizationSessionRepositoryInterface {
24
- constructor() {
25
- super();
26
- this.prisma = prisma;
27
- this.encryptionService = new DocumentDBEncryptionService();
28
- }
29
-
30
- async create(session) {
31
- const now = new Date();
32
- const document = await this.encryptionService.encryptFields(COLLECTION, {
33
- sessionId: session.sessionId,
34
- userId: session.userId,
35
- entityType: session.entityType,
36
- credentialId: session.credentialId,
37
- currentStep: session.currentStep,
38
- maxSteps: session.maxSteps,
39
- stepData: session.stepData,
40
- expiresAt: session.expiresAt,
41
- completed: session.completed,
42
- createdAt: now,
43
- updatedAt: now,
44
- });
45
- await insertOne(this.prisma, COLLECTION, document);
46
- return this._toDomain(
47
- await this.encryptionService.decryptFields(COLLECTION, document)
48
- );
49
- }
50
-
51
- async findBySessionId(sessionId) {
52
- if (typeof sessionId !== 'string' || !sessionId) return null;
53
- const document = await findOne(this.prisma, COLLECTION, {
54
- sessionId,
55
- expiresAt: { $gt: new Date() },
56
- });
57
- if (!document) return null;
58
- return this._toDomain(
59
- await this.encryptionService.decryptFields(COLLECTION, document)
60
- );
61
- }
62
-
63
- async update(session) {
64
- const encrypted = await this.encryptionService.encryptFields(COLLECTION, {
65
- stepData: session.stepData,
66
- });
67
- await updateOne(
68
- this.prisma,
69
- COLLECTION,
70
- { sessionId: session.sessionId },
71
- {
72
- $set: {
73
- currentStep: session.currentStep,
74
- stepData: encrypted.stepData,
75
- completed: session.completed,
76
- updatedAt: new Date(),
77
- },
78
- }
79
- );
80
- return session;
81
- }
82
-
83
- async deleteBySessionId(sessionId) {
84
- const result = await deleteMany(this.prisma, COLLECTION, { sessionId });
85
- return result?.n ?? 0;
86
- }
87
-
88
- async deleteExpired(now = new Date()) {
89
- const result = await deleteMany(this.prisma, COLLECTION, {
90
- expiresAt: { $lte: now },
91
- });
92
- return result?.n ?? 0;
93
- }
94
-
95
- _toDomain(document) {
96
- return new AuthorizationSession({
97
- sessionId: document.sessionId,
98
- userId: document.userId,
99
- entityType: document.entityType,
100
- credentialId: document.credentialId ?? null,
101
- currentStep: document.currentStep,
102
- maxSteps: document.maxSteps,
103
- stepData: document.stepData || {},
104
- expiresAt: new Date(document.expiresAt),
105
- completed: Boolean(document.completed),
106
- createdAt: document.createdAt,
107
- updatedAt: document.updatedAt,
108
- });
109
- }
110
- }
111
-
112
- module.exports = { AuthorizationSessionRepositoryDocumentDB };
@@ -1,31 +0,0 @@
1
- const config = require('../../database/config');
2
- const {
3
- AuthorizationSessionRepositoryPrisma,
4
- } = require('./authorization-session-repository-prisma');
5
- const {
6
- AuthorizationSessionRepositoryDocumentDB,
7
- } = require('./authorization-session-repository-documentdb');
8
-
9
- /**
10
- * AuthorizationSession repository for the configured database. PostgreSQL
11
- * and MongoDB share the Prisma adapter; DocumentDB uses raw commands.
12
- */
13
- function createAuthorizationSessionRepository() {
14
- switch (config.DB_TYPE) {
15
- case 'mongodb':
16
- case 'postgresql':
17
- return new AuthorizationSessionRepositoryPrisma();
18
- case 'documentdb':
19
- return new AuthorizationSessionRepositoryDocumentDB();
20
- default:
21
- throw new Error(
22
- `Unsupported database type: ${config.DB_TYPE}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
23
- );
24
- }
25
- }
26
-
27
- module.exports = {
28
- createAuthorizationSessionRepository,
29
- AuthorizationSessionRepositoryPrisma,
30
- AuthorizationSessionRepositoryDocumentDB,
31
- };
@@ -1,47 +0,0 @@
1
- /**
2
- * Port for persisting AuthorizationSession records (Management API v2
3
- * multi-step authorization). Adapters return AuthorizationSession domain
4
- * objects and never expired sessions from lookups.
5
- */
6
- class AuthorizationSessionRepositoryInterface {
7
- /**
8
- * @param {import('../domain/authorization-session').AuthorizationSession} session
9
- * @returns {Promise<import('../domain/authorization-session').AuthorizationSession>}
10
- */
11
- async create(session) {
12
- throw new Error('Method create must be implemented by subclass');
13
- }
14
-
15
- /**
16
- * @param {string} sessionId
17
- * @returns {Promise<import('../domain/authorization-session').AuthorizationSession|null>}
18
- * null when missing or expired.
19
- */
20
- async findBySessionId(sessionId) {
21
- throw new Error('Method findBySessionId must be implemented by subclass');
22
- }
23
-
24
- /**
25
- * Saves currentStep, stepData and completed.
26
- * @param {import('../domain/authorization-session').AuthorizationSession} session
27
- */
28
- async update(session) {
29
- throw new Error('Method update must be implemented by subclass');
30
- }
31
-
32
- /** @param {string} sessionId */
33
- async deleteBySessionId(sessionId) {
34
- throw new Error('Method deleteBySessionId must be implemented by subclass');
35
- }
36
-
37
- /**
38
- * Removes sessions that expired before `now`.
39
- * @param {Date} [now]
40
- * @returns {Promise<number>} how many were removed
41
- */
42
- async deleteExpired(now) {
43
- throw new Error('Method deleteExpired must be implemented by subclass');
44
- }
45
- }
46
-
47
- module.exports = { AuthorizationSessionRepositoryInterface };