@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,87 +0,0 @@
1
- const { prisma } = require('../../database/prisma');
2
- const { AuthorizationSession } = require('../domain/authorization-session');
3
- const {
4
- AuthorizationSessionRepositoryInterface,
5
- } = require('./authorization-session-repository-interface');
6
-
7
- /**
8
- * AuthorizationSession persistence through the Prisma client, for PostgreSQL
9
- * and MongoDB (both schemas declare the same model; the session id, not the
10
- * row id, is the key, so no id conversion is needed). `stepData` is
11
- * encrypted by the Prisma encryption extension (encryption-schema-registry).
12
- */
13
- class AuthorizationSessionRepositoryPrisma extends AuthorizationSessionRepositoryInterface {
14
- constructor(prismaClient = prisma) {
15
- super();
16
- this.prisma = prismaClient;
17
- }
18
-
19
- async create(session) {
20
- const record = await this.prisma.authorizationSession.create({
21
- data: {
22
- sessionId: session.sessionId,
23
- userId: session.userId,
24
- entityType: session.entityType,
25
- credentialId: session.credentialId,
26
- currentStep: session.currentStep,
27
- maxSteps: session.maxSteps,
28
- stepData: session.stepData,
29
- expiresAt: session.expiresAt,
30
- completed: session.completed,
31
- },
32
- });
33
- return toDomain(record);
34
- }
35
-
36
- async findBySessionId(sessionId) {
37
- if (typeof sessionId !== 'string' || !sessionId) return null;
38
- const record = await this.prisma.authorizationSession.findFirst({
39
- where: { sessionId, expiresAt: { gt: new Date() } },
40
- });
41
- return record ? toDomain(record) : null;
42
- }
43
-
44
- async update(session) {
45
- const record = await this.prisma.authorizationSession.update({
46
- where: { sessionId: session.sessionId },
47
- data: {
48
- currentStep: session.currentStep,
49
- stepData: session.stepData,
50
- completed: session.completed,
51
- },
52
- });
53
- return toDomain(record);
54
- }
55
-
56
- async deleteBySessionId(sessionId) {
57
- const result = await this.prisma.authorizationSession.deleteMany({
58
- where: { sessionId },
59
- });
60
- return result.count;
61
- }
62
-
63
- async deleteExpired(now = new Date()) {
64
- const result = await this.prisma.authorizationSession.deleteMany({
65
- where: { expiresAt: { lte: now } },
66
- });
67
- return result.count;
68
- }
69
- }
70
-
71
- function toDomain(record) {
72
- return new AuthorizationSession({
73
- sessionId: record.sessionId,
74
- userId: record.userId,
75
- entityType: record.entityType,
76
- credentialId: record.credentialId ?? null,
77
- currentStep: record.currentStep,
78
- maxSteps: record.maxSteps,
79
- stepData: record.stepData || {},
80
- expiresAt: record.expiresAt,
81
- completed: record.completed,
82
- createdAt: record.createdAt,
83
- updatedAt: record.updatedAt,
84
- });
85
- }
86
-
87
- module.exports = { AuthorizationSessionRepositoryPrisma, toDomain };
@@ -1,18 +0,0 @@
1
- /**
2
- * The error `findEntityById` throws for an id with no row, and the one
3
- * predicate that recognises it (as integration-not-found.js does for
4
- * integrations). The adapters build the error here and use cases match it
5
- * here, so a missing entity can be told from any other failure without
6
- * widening a regex.
7
- *
8
- * Why it matters: a use case that answers 404 for another tenant's entity must
9
- * answer the same 404 for an id that does not exist. Otherwise the status code
10
- * says which ids exist (ADR-052 §3, ADR-056 §3.6).
11
- */
12
- const MESSAGE = /^Entity .+ not found$/;
13
-
14
- const entityNotFoundError = (id) => new Error(`Entity ${id} not found`);
15
-
16
- const isEntityNotFound = (error) => MESSAGE.test(error?.message || '');
17
-
18
- module.exports = { entityNotFoundError, isEntityNotFound };
@@ -1,244 +0,0 @@
1
- const { FetchError } = require('../../errors/fetch-error');
2
- const { ProxyRequestError } = require('../proxy/proxy-errors');
3
- const {
4
- normalizeProxyPath,
5
- joinProxyUrl,
6
- } = require('../proxy/proxy-path');
7
-
8
- const MAX_REDIRECTS = 3;
9
-
10
- /**
11
- * The generic, path-checked request behind the Management API entity proxy
12
- * (ADR-052 §8). It runs through the requester's own `_request`, so auth
13
- * headers, token refresh (single-flight), the per-attempt timeout, telemetry
14
- * and log redaction all apply. What it adds:
15
- *
16
- * - the URL is always `module base URL + normalised path`; the caller cannot
17
- * name a host, scheme or port;
18
- * - no backoff retries (the caller decides; a 429 comes back with its
19
- * Retry-After), and upstream errors are returned, not thrown;
20
- * - redirects are handled manually: same origin and under the base path only,
21
- * at most three hops, each re-checked with `isAllowedRedirect`;
22
- * - one deadline for the whole call and a cap on the response body.
23
- *
24
- * @param {import('./requester').Requester} requester
25
- * @param {Object} params
26
- * @param {string} params.method - upper case
27
- * @param {string} params.path - caller path, validated here
28
- * @param {Object} [params.query]
29
- * @param {Object} [params.headers] - already filtered by the caller's policy
30
- * @param {string|Buffer} [params.body] - already serialised
31
- * @param {string} [params.baseUrl] - module-declared proxy base URL; falls
32
- * back to the requester's `proxyBaseUrl`, `baseUrl` or `baseURL`
33
- * @param {number} params.timeoutMs - budget for the whole call
34
- * @param {number} params.maxResponseBytes
35
- * @param {(path: string, method: string) => boolean} [params.isAllowedRedirect]
36
- * @returns {Promise<{ status: number, headers: Object, body: Buffer, contentType: string, redirectBlocked: boolean }>}
37
- * @throws {ProxyRequestError} INVALID_PROXY_REQUEST, PROXY_NOT_CONFIGURED,
38
- * INVALID_CREDENTIALS, TIMEOUT, NETWORK_ERROR, RESPONSE_TOO_LARGE
39
- */
40
- async function proxyRequest(requester, params) {
41
- const {
42
- method,
43
- path,
44
- query,
45
- headers = {},
46
- body,
47
- baseUrl,
48
- timeoutMs,
49
- maxResponseBytes,
50
- isAllowedRedirect,
51
- } = params;
52
-
53
- const normalizedPath = normalizeProxyPath(path);
54
- const base =
55
- baseUrl ??
56
- requester.proxyBaseUrl ??
57
- requester.baseUrl ??
58
- requester.baseURL;
59
- if (!base) {
60
- throw new ProxyRequestError(
61
- 'PROXY_NOT_CONFIGURED',
62
- 'The module declares no base URL to proxy to'
63
- );
64
- }
65
- const target = joinProxyUrl(base, normalizedPath);
66
- const deadline = Date.now() + timeoutMs;
67
-
68
- const run = async () => {
69
- let url = target.url;
70
- let currentMethod = method;
71
- let currentBody = body;
72
- let currentQuery = query;
73
- for (let hop = 0; ; hop++) {
74
- const remaining = deadline - Date.now();
75
- if (remaining <= 0) throw timeoutError();
76
-
77
- const response = await requester._request(url, {
78
- method: currentMethod,
79
- headers: { ...headers },
80
- query: currentQuery,
81
- body: currentBody,
82
- returnFullRes: true,
83
- returnErrorResponses: true,
84
- noRetry: true,
85
- redirect: 'manual',
86
- size: maxResponseBytes,
87
- timeoutMs: remaining,
88
- });
89
-
90
- const location = response.headers.get('location');
91
- if (response.status >= 300 && response.status < 400 && location) {
92
- const next = sameApiRedirect(location, url, target);
93
- const nextMethod = redirectMethod(response.status, currentMethod);
94
- if (
95
- next &&
96
- hop < MAX_REDIRECTS &&
97
- (!isAllowedRedirect || isAllowedRedirect(next.path, nextMethod))
98
- ) {
99
- if (nextMethod !== currentMethod) currentBody = undefined;
100
- currentMethod = nextMethod;
101
- url = next.url;
102
- currentQuery = undefined;
103
- continue;
104
- }
105
- return readResponse(response, maxResponseBytes, {
106
- redirectBlocked: !next,
107
- });
108
- }
109
- return readResponse(response, maxResponseBytes, {
110
- redirectBlocked: false,
111
- });
112
- }
113
- };
114
-
115
- let timer;
116
- const timeout = new Promise((_, reject) => {
117
- timer = setTimeout(() => reject(timeoutError()), timeoutMs);
118
- });
119
- const work = run();
120
- // If the deadline wins, the request still settles later; nothing awaits it.
121
- work.catch(() => {});
122
- try {
123
- return await Promise.race([work, timeout]);
124
- } catch (error) {
125
- throw toProxyError(error);
126
- } finally {
127
- clearTimeout(timer);
128
- }
129
- }
130
-
131
- function timeoutError() {
132
- return new ProxyRequestError('TIMEOUT', 'The upstream API did not answer in time');
133
- }
134
-
135
- /**
136
- * A redirect target the proxy may follow: same origin as the module base URL
137
- * and under its base path. Returns null for anything else.
138
- */
139
- function sameApiRedirect(location, currentUrl, target) {
140
- let next;
141
- try {
142
- next = new URL(location, currentUrl);
143
- } catch {
144
- return null;
145
- }
146
- if (next.origin !== target.origin || next.protocol !== 'https:') return null;
147
- let decodedPath;
148
- try {
149
- decodedPath = decodeURIComponent(next.pathname);
150
- } catch {
151
- return null;
152
- }
153
- if (
154
- !(decodedPath === target.basePath || decodedPath.startsWith(`${target.basePath}/`))
155
- ) {
156
- return null;
157
- }
158
- const relativePath = decodedPath.slice(target.basePath.length) || '/';
159
- let checkedPath;
160
- try {
161
- checkedPath = normalizeProxyPath(relativePath);
162
- } catch {
163
- return null;
164
- }
165
- // _rawRequest percent-encodes the URL once, so hand it the decoded form.
166
- return {
167
- url: `${target.origin}${target.basePath}${checkedPath}${next.search ? decodeURI(next.search) : ''}`,
168
- path: checkedPath,
169
- };
170
- }
171
-
172
- /** Method for the next hop, as fetch does it. */
173
- function redirectMethod(status, method) {
174
- if (status === 303 && method !== 'HEAD') return 'GET';
175
- if ((status === 301 || status === 302) && method === 'POST') return 'GET';
176
- return method;
177
- }
178
-
179
- async function readResponse(response, maxResponseBytes, { redirectBlocked }) {
180
- const declaredLength = Number(response.headers.get('content-length'));
181
- if (Number.isFinite(declaredLength) && declaredLength > maxResponseBytes) {
182
- destroyBody(response);
183
- throw tooLarge();
184
- }
185
- const buffer = Buffer.from(await response.arrayBuffer());
186
- if (buffer.length > maxResponseBytes) throw tooLarge();
187
- const headers = {};
188
- response.headers.forEach((value, name) => {
189
- headers[name.toLowerCase()] = value;
190
- });
191
- return {
192
- status: response.status,
193
- headers,
194
- body: buffer,
195
- contentType: headers['content-type'] || '',
196
- redirectBlocked,
197
- };
198
- }
199
-
200
- function destroyBody(response) {
201
- try {
202
- response.body?.destroy?.();
203
- } catch {
204
- // nothing to clean up
205
- }
206
- }
207
-
208
- function tooLarge() {
209
- return new ProxyRequestError(
210
- 'RESPONSE_TOO_LARGE',
211
- 'The upstream response is larger than the proxy allows'
212
- );
213
- }
214
-
215
- /** Maps requester and fetch failures to the proxy's error codes. */
216
- function toProxyError(error) {
217
- if (error instanceof ProxyRequestError) return error;
218
- const causeType = error?.cause?.type ?? error?.type;
219
- if (causeType === 'max-size') return tooLarge();
220
- if (error?.isTimeout) return timeoutError();
221
- if (error instanceof FetchError || error?.name === 'FetchError') {
222
- if (error.statusCode === 401) {
223
- return new ProxyRequestError(
224
- 'INVALID_CREDENTIALS',
225
- 'The upstream API rejected the stored credentials; re-authorize the entity',
226
- { upstreamStatus: 401 }
227
- );
228
- }
229
- if (!error.statusCode) {
230
- return new ProxyRequestError(
231
- 'NETWORK_ERROR',
232
- 'Could not reach the upstream API'
233
- );
234
- }
235
- return new ProxyRequestError(
236
- 'UPSTREAM_ERROR',
237
- 'The upstream API returned an error',
238
- { upstreamStatus: error.statusCode }
239
- );
240
- }
241
- return error;
242
- }
243
-
244
- module.exports = { proxyRequest, MAX_REDIRECTS };
@@ -1,192 +0,0 @@
1
- /**
2
- * Reading and validating ownership settings in an app definition (ADR-056
3
- * §3.1, §3.3). Called once by loadAppDefinition, so a bad setting stops the app
4
- * at load, not on the first request. No I/O.
5
- */
6
-
7
- const {
8
- SCOPES,
9
- INDIVIDUAL_ACROSS_VALUES,
10
- OWNER_RULES,
11
- DEFAULT_ORG_ADMIN_ROLES,
12
- configError,
13
- isPlainObject,
14
- parseScopeSetting,
15
- resolveScope,
16
- } = require('./scope-resolution');
17
- const { entityTypeNameOf } = require('./entity-type-registry');
18
-
19
- /** The binding scope an integration declares for `modules[key]`, if any. */
20
- function bindingScopeSetting(Definition, key) {
21
- const binding = Definition?.modules?.[key];
22
- if (binding?.scope !== undefined) return binding.scope;
23
- const entityEntry = Definition?.entities?.[key];
24
- if (entityEntry?.scope !== undefined) return entityEntry.scope;
25
- // Deprecated alias from the global-entities branch: `global: true`.
26
- if (entityEntry?.global === true) return SCOPES.GLOBAL;
27
- return undefined;
28
- }
29
-
30
- function readAppOwnership(appDefinition) {
31
- const raw = appDefinition?.ownership;
32
- if (raw === undefined || raw === null) {
33
- return { scope: null, individualAcross: 'organization' };
34
- }
35
- if (!isPlainObject(raw)) {
36
- throw configError('appDefinition.ownership', 'must be an object');
37
- }
38
- const unknown = Object.keys(raw).filter(
39
- (key) => key !== 'scope' && key !== 'individualAcross'
40
- );
41
- if (unknown.length) {
42
- throw configError(
43
- 'appDefinition.ownership',
44
- `unknown setting(s): ${unknown.join(', ')}`
45
- );
46
- }
47
- const individualAcross = raw.individualAcross ?? 'organization';
48
- if (!INDIVIDUAL_ACROSS_VALUES.includes(individualAcross)) {
49
- throw configError(
50
- 'appDefinition.ownership.individualAcross',
51
- `must be one of ${INDIVIDUAL_ACROSS_VALUES.join(', ')}`
52
- );
53
- }
54
- return {
55
- scope: parseScopeSetting(raw.scope, 'appDefinition.ownership.scope'),
56
- individualAcross,
57
- };
58
- }
59
-
60
- function readIntegrationOwnership(integrationClass) {
61
- const name = integrationClass?.Definition?.name || 'unnamed';
62
- const raw = integrationClass?.Definition?.ownership;
63
- if (raw === undefined || raw === null) return { scope: null, owner: 'derive' };
64
- const where = `integration '${name}' ownership`;
65
- if (!isPlainObject(raw)) throw configError(where, 'must be an object');
66
- const unknown = Object.keys(raw).filter(
67
- (key) => key !== 'scope' && key !== 'owner'
68
- );
69
- if (unknown.length) {
70
- throw configError(where, `unknown setting(s): ${unknown.join(', ')}`);
71
- }
72
- const owner = raw.owner ?? 'derive';
73
- if (!OWNER_RULES.includes(owner)) {
74
- throw configError(where, `owner must be one of ${OWNER_RULES.join(', ')}`);
75
- }
76
- return { scope: parseScopeSetting(raw.scope, `${where}.scope`), owner };
77
- }
78
-
79
- /**
80
- * `user.orgAdminRoles`: the token roles that may manage organization-level
81
- * things. Default `['ADMIN']`. A shared-secret caller needs no role.
82
- */
83
- function readOrgAdminRoles(appDefinition) {
84
- const raw = appDefinition?.user?.orgAdminRoles;
85
- if (raw === undefined || raw === null) return [...DEFAULT_ORG_ADMIN_ROLES];
86
- if (
87
- !Array.isArray(raw) ||
88
- raw.length === 0 ||
89
- raw.some((role) => typeof role !== 'string' || role.trim() === '')
90
- ) {
91
- throw configError(
92
- 'appDefinition.user.orgAdminRoles',
93
- 'must be a non-empty array of role names'
94
- );
95
- }
96
- return [...raw];
97
- }
98
-
99
- /**
100
- * `user.orgAdminGateBearerTokens`: judge Frigg bearer-token callers by role
101
- * like adopter-token callers. Default false: they keep their pre-roles
102
- * behaviour and may manage the organization they act in.
103
- */
104
- function readGateBearerTokens(appDefinition) {
105
- const raw = appDefinition?.user?.orgAdminGateBearerTokens;
106
- if (raw === undefined || raw === null) return false;
107
- if (typeof raw !== 'boolean') {
108
- throw configError('appDefinition.user.orgAdminGateBearerTokens', 'must be a boolean');
109
- }
110
- return raw;
111
- }
112
-
113
- /**
114
- * Every binding of every integration with its resolved scope and where it came
115
- * from, parsing each integration's settings once. Throws OwnershipConfigError
116
- * for any invalid one. Exported for tooling that wants to show the chain.
117
- */
118
- function listBindingScopes(appDefinition, entityTypeRegistry, primary) {
119
- const app = readAppOwnership(appDefinition);
120
- const rows = [];
121
- for (const integrationClass of appDefinition?.integrations || []) {
122
- const Definition = integrationClass?.Definition;
123
- const name = Definition?.name || 'unnamed';
124
- const ownership = readIntegrationOwnership(integrationClass);
125
- for (const [key, binding] of Object.entries(Definition?.modules || {})) {
126
- const moduleName = entityTypeNameOf(binding?.definition);
127
- const resolved = resolveScope({
128
- binding: parseScopeSetting(
129
- bindingScopeSetting(Definition, key),
130
- `integration '${name}' binding '${key}' scope`
131
- ),
132
- integration: ownership.scope,
133
- entityType: moduleName
134
- ? entityTypeRegistry?.get(moduleName)?.settings?.scope
135
- : undefined,
136
- app: app.scope,
137
- primary,
138
- });
139
- rows.push({
140
- integration: name,
141
- binding: key,
142
- moduleName: moduleName ?? null,
143
- scope: resolved.scope,
144
- source: resolved.source,
145
- owner: ownership.owner,
146
- });
147
- }
148
- }
149
- return rows;
150
- }
151
-
152
- /**
153
- * Validates every ownership setting in one pass and returns the app-level
154
- * config the runtime needs.
155
- *
156
- * @returns {{scope: ({default: string, allow: string[]}|null), individualAcross: string, orgAdminRoles: string[], gateBearerTokens: boolean}}
157
- * @throws {OwnershipConfigError}
158
- */
159
- function resolveOwnershipConfig(appDefinition, entityTypeRegistry) {
160
- const app = readAppOwnership(appDefinition);
161
- for (const [name, entry] of entityTypeRegistry || []) {
162
- parseScopeSetting(entry?.settings?.scope, `entity type '${name}' scope`);
163
- }
164
- const rows = listBindingScopes(
165
- appDefinition,
166
- entityTypeRegistry,
167
- appDefinition?.user?.primary
168
- );
169
- // §3.3: a shared integration must not run on one person's credential.
170
- for (const row of rows) {
171
- if (row.owner === 'organization' && row.scope === SCOPES.INDIVIDUAL) {
172
- throw configError(
173
- `integration '${row.integration}' binding '${row.binding}'`,
174
- "an organization-owned integration cannot bind an 'individual' entity"
175
- );
176
- }
177
- }
178
- return Object.freeze({
179
- scope: app.scope,
180
- individualAcross: app.individualAcross,
181
- orgAdminRoles: Object.freeze(readOrgAdminRoles(appDefinition)),
182
- gateBearerTokens: readGateBearerTokens(appDefinition),
183
- });
184
- }
185
-
186
- module.exports = {
187
- bindingScopeSetting,
188
- readAppOwnership,
189
- readIntegrationOwnership,
190
- listBindingScopes,
191
- resolveOwnershipConfig,
192
- };
@@ -1,112 +0,0 @@
1
- /**
2
- * The object a use case asks about scopes (ADR-056 §3): the app's ownership
3
- * config and entity type registry bound once, as closures. With no arguments it
4
- * is the behaviour of an app that configures nothing.
5
- */
6
-
7
- const {
8
- INDIVIDUAL_ACROSS_VALUES,
9
- DEFAULT_ORG_ADMIN_ROLES,
10
- configError,
11
- parseScopeSetting,
12
- resolveScope,
13
- resolveOwnerForScope,
14
- } = require('./scope-resolution');
15
- const {
16
- accessContext,
17
- accessToOwner,
18
- describeRecord,
19
- } = require('./scope-visibility');
20
-
21
- /**
22
- * @param {Object} [params]
23
- * @param {{scope: *, individualAcross: string, orgAdminRoles?: string[], gateBearerTokens?: boolean}} [params.ownership] - the raw
24
- * `appDefinition.ownership` or what resolveOwnershipConfig returned (a parsed
25
- * `{ default, allow }` parses to itself)
26
- * @param {Map} [params.entityTypeRegistry]
27
- */
28
- function createScopePolicy({ ownership, entityTypeRegistry } = {}) {
29
- const appScope = parseScopeSetting(
30
- ownership?.scope,
31
- 'appDefinition.ownership.scope'
32
- );
33
- const individualAcross = ownership?.individualAcross ?? 'organization';
34
- if (!INDIVIDUAL_ACROSS_VALUES.includes(individualAcross)) {
35
- throw configError(
36
- 'appDefinition.ownership.individualAcross',
37
- `must be one of ${INDIVIDUAL_ACROSS_VALUES.join(', ')}`
38
- );
39
- }
40
-
41
- const orgAdminRoles = ownership?.orgAdminRoles ?? DEFAULT_ORG_ADMIN_ROLES;
42
- const gateBearerTokens = ownership?.gateBearerTokens === true;
43
-
44
- const contextFor = (user) =>
45
- accessContext(user, { individualAcross, orgAdminRoles, gateBearerTokens });
46
-
47
- /** The scope a new connection of `entityType` gets (levels 1, 4, 5, 6). */
48
- const resolveEntityScope = ({ entityType, requestScope, binding, integration, user }) =>
49
- resolveScope({
50
- request: requestScope,
51
- binding,
52
- integration,
53
- entityType: entityTypeRegistry?.get?.(entityType)?.settings?.scope,
54
- app: appScope,
55
- primary: user?.config?.primary,
56
- });
57
-
58
- /**
59
- * Scope, then owner and organization, for a new entity. `legacyOrgId` (the
60
- * person's legacy organization pointer) rides along so a later lookup can
61
- * reuse an entity written before scoping.
62
- */
63
- const resolveOwnership = ({ entityType, requestScope, user }) => {
64
- const ctx = contextFor(user);
65
- const resolved = resolveEntityScope({ entityType, requestScope, user });
66
- return {
67
- ...resolveOwnerForScope(resolved.scope, ctx),
68
- legacyOrgId: ctx.legacyOrgId,
69
- source: resolved.source,
70
- };
71
- };
72
-
73
- /**
74
- * The owner and organization for a scope that is already decided: a
75
- * re-authorization keeps the scope its connection has, whatever the config
76
- * says now. The admin gate applies as it does on create.
77
- */
78
- const ownershipForScope = ({ scope, user }) => {
79
- const ctx = contextFor(user);
80
- return {
81
- ...resolveOwnerForScope(scope, ctx),
82
- legacyOrgId: ctx.legacyOrgId,
83
- source: 'fixed',
84
- };
85
- };
86
-
87
- /** Whether `user` may load this stored entity for use (same clauses as the list). */
88
- const canUseEntity = (user, entity) =>
89
- describeRecord(entity, contextFor(user), 'entity') !== null;
90
-
91
- /**
92
- * What `user` may do with a record owned by `ownerId` that has no
93
- * organization of its own (a credential): manage, or null when not theirs.
94
- */
95
- const accessToOwnedBy = (user, ownerId) => accessToOwner(ownerId, contextFor(user));
96
-
97
- return Object.freeze({
98
- individualAcross,
99
- orgAdminRoles,
100
- accessToOwnedBy,
101
- accessContext: contextFor,
102
- resolveScope: resolveEntityScope,
103
- resolveOwnership,
104
- ownershipForScope,
105
- canUseEntity,
106
- });
107
- }
108
-
109
- /** The policy of an app that configures nothing; what a use case gets by default. */
110
- const DEFAULT_SCOPE_POLICY = createScopePolicy();
111
-
112
- module.exports = { createScopePolicy, DEFAULT_SCOPE_POLICY };