@friggframework/core 2.0.0--canary.680.6bacd26.0 → 2.0.0--canary.663.8de669c.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +0 -11
  2. package/application/commands/integration-commands.js +8 -18
  3. package/application/index.js +2 -6
  4. package/credential/repositories/credential-repository-documentdb.js +0 -34
  5. package/credential/repositories/credential-repository-interface.js +0 -15
  6. package/credential/repositories/credential-repository-mongo.js +0 -23
  7. package/credential/repositories/credential-repository-postgres.js +0 -28
  8. package/database/encryption/README.md +0 -2
  9. package/database/encryption/encryption-schema-registry.js +12 -41
  10. package/database/encryption/field-encryption-service.js +5 -69
  11. package/generated/prisma-mongodb/edge.js +4 -20
  12. package/generated/prisma-mongodb/index-browser.js +1 -17
  13. package/generated/prisma-mongodb/index.d.ts +95 -1543
  14. package/generated/prisma-mongodb/index.js +4 -20
  15. package/generated/prisma-mongodb/package.json +1 -1
  16. package/generated/prisma-mongodb/schema.prisma +0 -27
  17. package/generated/prisma-mongodb/wasm.js +4 -20
  18. package/generated/prisma-postgresql/edge.js +4 -20
  19. package/generated/prisma-postgresql/index-browser.js +1 -17
  20. package/generated/prisma-postgresql/index.d.ts +84 -1565
  21. package/generated/prisma-postgresql/index.js +4 -20
  22. package/generated/prisma-postgresql/package.json +1 -1
  23. package/generated/prisma-postgresql/schema.prisma +0 -26
  24. package/generated/prisma-postgresql/wasm.js +4 -20
  25. package/handlers/app-definition-loader.js +8 -19
  26. package/handlers/app-handler-helpers.js +3 -25
  27. package/handlers/backend-utils.js +25 -31
  28. package/handlers/middleware/admin-auth.js +1 -14
  29. package/handlers/routers/auth.js +3 -11
  30. package/handlers/routers/health.js +4 -11
  31. package/handlers/workers/integration-defined-workers.js +2 -5
  32. package/index.js +2 -4
  33. package/integrations/index.js +4 -0
  34. package/integrations/integration-router.js +23 -16
  35. package/integrations/utils/map-integration-dto.js +15 -1
  36. package/modules/index.js +0 -6
  37. package/modules/requester/requester.js +2 -27
  38. package/package.json +5 -5
  39. package/prisma-mongodb/schema.prisma +0 -27
  40. package/prisma-postgresql/schema.prisma +0 -26
  41. package/types/index.d.ts +0 -1
  42. package/credential/use-cases/credential-operations-for-user.js +0 -117
  43. package/credential/use-cases/reauthorize-credential-for-user.js +0 -66
  44. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  45. package/integrations/use-cases/integration-operations-for-user.js +0 -225
  46. package/management-api/create-management-api-router.js +0 -49
  47. package/management-api/error-response.js +0 -116
  48. package/management-api/management-api-config.js +0 -82
  49. package/management-api/meta-router.js +0 -57
  50. package/management-api/openapi/build-openapi.js +0 -144
  51. package/management-api/openapi/components.js +0 -368
  52. package/management-api/openapi/v1-operations.js +0 -83
  53. package/management-api/openapi/v2-operations.js +0 -252
  54. package/management-api/route-registry.js +0 -294
  55. package/management-api/test-support/http-client.js +0 -39
  56. package/management-api/use-cases/get-management-api-meta.js +0 -57
  57. package/management-api/v1-deprecation.js +0 -120
  58. package/management-api/v2/create-v2-router.js +0 -85
  59. package/management-api/v2/dependencies.js +0 -256
  60. package/management-api/v2/dto.js +0 -123
  61. package/management-api/v2/handlers/authorization.js +0 -93
  62. package/management-api/v2/handlers/credentials.js +0 -29
  63. package/management-api/v2/handlers/entities.js +0 -74
  64. package/management-api/v2/handlers/integrations.js +0 -125
  65. package/management-api/v2/handlers/proxy.js +0 -34
  66. package/management-api/v2/request.js +0 -54
  67. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  68. package/modules/domain/authorization-session.js +0 -144
  69. package/modules/entity-type-registry.js +0 -311
  70. package/modules/proxy/proxy-errors.js +0 -20
  71. package/modules/proxy/proxy-path.js +0 -179
  72. package/modules/proxy/proxy-policy.js +0 -248
  73. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  74. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  75. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  76. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  77. package/modules/requester/proxy-request.js +0 -244
  78. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  79. package/modules/use-cases/entity-operations-for-user.js +0 -125
  80. package/modules/use-cases/entity-types.js +0 -115
  81. package/modules/use-cases/execute-entity-proxy-request.js +0 -239
  82. package/modules/use-cases/get-authorization-requirements.js +0 -100
  83. package/modules/use-cases/get-entity-module-for-user.js +0 -86
  84. package/modules/use-cases/multi-step-authorization.js +0 -302
  85. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  86. package/types/modules/index.d.ts +0 -58
@@ -1,239 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { getLogger } = require('../../logs');
3
- const { getTelemetry } = require('../../telemetry/telemetry-runtime');
4
- const { ProxyRequestError } = require('../proxy/proxy-errors');
5
- const { normalizeProxyPath, matchAllowRule } = require('../proxy/proxy-path');
6
- const {
7
- validateProxyRequest,
8
- resolveAllowRules,
9
- filterRequestHeaders,
10
- filterResponseHeaders,
11
- decodeResponseBody,
12
- } = require('../proxy/proxy-policy');
13
-
14
- const log = getLogger('frigg.api.proxy');
15
-
16
- /** HTTP status and audit outcome for each proxy error code (ADR-052 §10). */
17
- const ERROR_MAP = {
18
- INVALID_PROXY_REQUEST: { status: 400, outcome: 'denied' },
19
- PROXY_NOT_ALLOWED: { status: 403, outcome: 'denied' },
20
- PAYLOAD_TOO_LARGE: { status: 413, outcome: 'too_large' },
21
- UNSUPPORTED_MEDIA_TYPE: { status: 415, outcome: 'denied' },
22
- PROXY_NOT_CONFIGURED: { status: 501, outcome: 'denied' },
23
- INVALID_CREDENTIALS: { status: 401, outcome: 'upstream_error' },
24
- RATE_LIMITED: { status: 429, outcome: 'upstream_error' },
25
- UPSTREAM_ERROR: { status: 502, outcome: 'upstream_error' },
26
- NETWORK_ERROR: { status: 502, outcome: 'upstream_error' },
27
- RESPONSE_TOO_LARGE: { status: 502, outcome: 'too_large' },
28
- TIMEOUT: { status: 504, outcome: 'timeout' },
29
- };
30
-
31
- function toBoom(error, extra = {}) {
32
- const mapping = ERROR_MAP[error.code] || { status: 502 };
33
- const boom = new Boom.Boom(error.message, {
34
- statusCode: mapping.status,
35
- data: {
36
- code: error.code,
37
- details: error.details,
38
- // Every proxy message is written for the caller (no URLs, no
39
- // credential material), including the 5xx ones.
40
- expose: true,
41
- proxy: true,
42
- ...extra,
43
- },
44
- });
45
- return boom;
46
- }
47
-
48
- const authHeaderNamesOf = (api) =>
49
- [api?.api_key_name, api?.API_KEY_NAME].filter(
50
- (name) => typeof name === 'string' && name
51
- );
52
-
53
- /**
54
- * POST /api/v2/entities/:entityId/proxy (ADR-052): calls the entity's API
55
- * through its module with the stored credential, inside the limits the app
56
- * and the module set.
57
- *
58
- * Order of checks: request shape and size, path rules, entity ownership
59
- * (404), the module's allow-list (403, deny by default), credential
60
- * readiness (401), then the call through Requester._proxyRequest. Every call,
61
- * allowed or denied, writes one audit record and one usage count. Bodies,
62
- * headers, query values and the raw path are never logged.
63
- */
64
- class ExecuteEntityProxyRequest {
65
- /**
66
- * @param {Object} params
67
- * @param {import('./get-entity-module-for-user').GetEntityModuleForUser} params.getEntityModuleForUser
68
- * @param {ReturnType<import('../../management-api/management-api-config').resolveManagementApiConfig>['proxy']} params.proxyConfig
69
- * @param {Object} [params.telemetry]
70
- * @param {() => number} [params.now]
71
- */
72
- constructor({ getEntityModuleForUser, proxyConfig, telemetry, now }) {
73
- this.getEntityModuleForUser = getEntityModuleForUser;
74
- this.proxyConfig = proxyConfig;
75
- this.telemetry = telemetry;
76
- this.now = now || Date.now;
77
- }
78
-
79
- /**
80
- * @param {string} entityId
81
- * @param {Object} user - authenticated User
82
- * @param {Object} request - { method, path, query?, headers?, body? }
83
- * @returns {Promise<{ status: number, body: { success: boolean, status: number, headers: Object, data: unknown, dataEncoding?: string } }>}
84
- */
85
- async execute(entityId, user, request) {
86
- const started = this.now();
87
- const audit = {
88
- userId: user?.getId?.(),
89
- organizationId: user?.getOrganizationUser?.()?.id,
90
- entityId: String(entityId),
91
- method: typeof request?.method === 'string' ? request.method.toUpperCase() : undefined,
92
- requestBytes: 0,
93
- };
94
-
95
- try {
96
- const validated = validateProxyRequest(request, this.proxyConfig);
97
- audit.method = validated.method;
98
- audit.requestBytes = validated.requestBytes;
99
- const path = normalizeProxyPath(validated.path);
100
-
101
- const { entity, definition, module } =
102
- await this.getEntityModuleForUser.execute(entityId, user);
103
- audit.module = definition.moduleName;
104
- const credentialRef = entity.credential;
105
- const credentialId =
106
- credentialRef && typeof credentialRef === 'object'
107
- ? credentialRef.id ?? credentialRef._id
108
- : credentialRef;
109
- if (credentialId !== undefined && credentialId !== null) {
110
- audit.credentialId = String(credentialId);
111
- }
112
-
113
- const { rules } = resolveAllowRules(definition, this.proxyConfig.modules);
114
- const rule = matchAllowRule(rules, validated.method, path);
115
- if (!rule) {
116
- throw new ProxyRequestError(
117
- 'PROXY_NOT_ALLOWED',
118
- `Module ${definition.moduleName} does not allow ${validated.method} on this path`,
119
- { module: definition.moduleName, method: validated.method }
120
- );
121
- }
122
- audit.pattern = rule.path;
123
-
124
- const credential = entity.credential;
125
- if (!credential || (typeof credential === 'object' && credential.authIsValid === false)) {
126
- throw new ProxyRequestError(
127
- 'INVALID_CREDENTIALS',
128
- 'The entity has no usable credential; re-authorize it'
129
- );
130
- }
131
-
132
- const authHeaderNames = authHeaderNamesOf(module.api);
133
- const headers = filterRequestHeaders(validated.headers, {
134
- moduleHeaders: Array.isArray(definition.proxy?.headers) ? definition.proxy.headers : [],
135
- authHeaderNames,
136
- });
137
- if (validated.body !== undefined && !headers['content-type']) {
138
- headers['content-type'] = validated.bodyIsJson ? 'application/json' : 'text/plain';
139
- }
140
-
141
- const upstream = await module.api._proxyRequest({
142
- method: validated.method,
143
- path,
144
- query: validated.query,
145
- headers,
146
- body: validated.body,
147
- baseUrl: this.resolveBaseUrl(definition, module.api),
148
- timeoutMs: this.proxyConfig.timeoutMs,
149
- maxResponseBytes: this.proxyConfig.maxResponseBytes,
150
- isAllowedRedirect: (nextPath, nextMethod) =>
151
- Boolean(matchAllowRule(rules, nextMethod, nextPath)),
152
- });
153
- audit.upstreamStatus = upstream.status;
154
- audit.responseBytes = upstream.body?.length ?? 0;
155
-
156
- if (upstream.status === 429) {
157
- throw Object.assign(
158
- new ProxyRequestError('RATE_LIMITED', 'The upstream API is rate limiting this account', {
159
- upstreamStatus: 429,
160
- }),
161
- { retryAfter: upstream.headers['retry-after'] }
162
- );
163
- }
164
- if (upstream.status >= 500) {
165
- throw new ProxyRequestError('UPSTREAM_ERROR', 'The upstream API returned an error', {
166
- upstreamStatus: upstream.status,
167
- });
168
- }
169
-
170
- const decoded = decodeResponseBody(upstream.body, upstream.contentType, {
171
- binary: definition.proxy?.binary === true,
172
- });
173
- const result = {
174
- status: upstream.status,
175
- body: {
176
- success: upstream.status < 400,
177
- status: upstream.status,
178
- headers: filterResponseHeaders(upstream.headers, {
179
- redirectBlocked: upstream.redirectBlocked,
180
- authHeaderNames,
181
- }),
182
- data: decoded.data,
183
- ...(decoded.encoding && { dataEncoding: decoded.encoding }),
184
- },
185
- };
186
- this.record(audit, upstream.status < 400 ? 'ok' : 'upstream_error', started);
187
- return result;
188
- } catch (error) {
189
- if (error instanceof ProxyRequestError) {
190
- const { outcome = 'upstream_error' } = ERROR_MAP[error.code] || {};
191
- this.record(audit, outcome, started, error.code);
192
- throw toBoom(error, error.retryAfter ? { retryAfter: error.retryAfter } : {});
193
- }
194
- const notFound = error?.isBoom && error.output.statusCode === 404;
195
- this.record(
196
- audit,
197
- notFound ? 'denied' : 'error',
198
- started,
199
- error?.data?.code || (error?.isBoom ? undefined : 'INTERNAL_ERROR')
200
- );
201
- throw error;
202
- }
203
- }
204
-
205
- /** A module may pin its proxy base URL (string, or a function of the api). */
206
- resolveBaseUrl(definition, api) {
207
- const declared = definition.proxy?.baseUrl;
208
- if (typeof declared === 'function') return declared(api);
209
- if (typeof declared === 'string') return declared;
210
- return undefined;
211
- }
212
-
213
- /** One audit record (ADR-048) and one usage count (ADR-011) per call. */
214
- record(audit, outcome, started, reason) {
215
- const fields = {
216
- eventName: 'frigg.api.proxy.request',
217
- ...audit,
218
- outcome,
219
- durationMs: this.now() - started,
220
- ...(reason && { reason }),
221
- };
222
- if (outcome === 'ok' || outcome === 'upstream_error') {
223
- log.info('Entity proxy request', fields);
224
- } else {
225
- log.warn('Entity proxy request refused', fields);
226
- }
227
- try {
228
- (this.telemetry || getTelemetry()).count('frigg.api.proxy.requests', 1, {
229
- module: audit.module || 'unknown',
230
- method: audit.method || 'unknown',
231
- outcome,
232
- });
233
- } catch {
234
- // Telemetry must never break a request.
235
- }
236
- }
237
- }
238
-
239
- module.exports = { ExecuteEntityProxyRequest, ERROR_MAP };
@@ -1,100 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { findDefinition, getAuthStepCount } = require('./entity-types');
3
-
4
- const defaultCreateModule = (params) => {
5
- const { Module } = require('../module');
6
- return new Module(params);
7
- };
8
-
9
- /**
10
- * Normalises what a module returns for a step into the v2 requirements
11
- * shape: `{ type, data }`. Modules either return `{ type, data }` already
12
- * (multi-step, JSON-schema forms) or a flat object such as the OAuth2
13
- * requester's `{ type: 'oauth2', url }`.
14
- */
15
- function normalizeRequirements(raw) {
16
- if (!raw || typeof raw !== 'object') return { type: 'custom', data: {} };
17
- if (raw.data && typeof raw.data === 'object') {
18
- return { ...raw, type: raw.type || 'custom' };
19
- }
20
- const { type, ...data } = raw;
21
- return { type: type || 'custom', data };
22
- }
23
-
24
- /**
25
- * Single-step requirements, from wherever the module declares them: a
26
- * requiredAuthMethods.getAuthorizationRequirements(api) (the form-based
27
- * API-key convention `frigg auth` uses), or the Api's own
28
- * getAuthorizationRequirements() (OAuth2Requester builds the authorize URL).
29
- */
30
- async function singleStepRequirements(definition, module) {
31
- const declared = definition.requiredAuthMethods?.getAuthorizationRequirements;
32
- if (typeof declared === 'function') return declared(module.api);
33
- if (typeof module.api?.getAuthorizationRequirements === 'function') {
34
- return module.api.getAuthorizationRequirements();
35
- }
36
- throw Boom.notImplemented(
37
- `Module ${definition.moduleName} declares no authorization requirements (add requiredAuthMethods.getAuthorizationRequirements)`,
38
- { code: 'REQUIREMENTS_NOT_DECLARED', expose: true }
39
- );
40
- }
41
-
42
- /**
43
- * What the caller must submit for one authorization step of an entity type
44
- * (Management API v2). Single-step modules use their Api's
45
- * getAuthorizationRequirements(); multi-step modules declare
46
- * getAuthStepCount() and getAuthRequirementsForStep(step) on their
47
- * definition (docs/MULTI_STEP_AUTH_MIGRATION_GUIDE.md).
48
- */
49
- class GetAuthorizationRequirements {
50
- /**
51
- * @param {Object} params
52
- * @param {Array<Object>} params.moduleDefinitions
53
- * @param {Function} [params.createModule]
54
- */
55
- constructor({ moduleDefinitions, createModule }) {
56
- this.moduleDefinitions = moduleDefinitions || [];
57
- this.createModule = createModule || defaultCreateModule;
58
- }
59
-
60
- /**
61
- * @param {Object} params
62
- * @param {string} params.entityType
63
- * @param {number} [params.step=1]
64
- * @param {string} [params.userId]
65
- * @param {string} [params.state] - OAuth state forwarded to the Api.
66
- * @returns {Promise<{type: string, data: Object, step: number, totalSteps: number, isMultiStep: boolean}>}
67
- */
68
- async execute({ entityType, step = 1, userId = null, state } = {}) {
69
- const definition = findDefinition(this.moduleDefinitions, entityType);
70
- const totalSteps = getAuthStepCount(definition);
71
-
72
- if (!Number.isInteger(step) || step < 1 || step > totalSteps) {
73
- throw Boom.badRequest(
74
- `step must be between 1 and ${totalSteps} for ${entityType}`,
75
- { code: 'INVALID_STEP' }
76
- );
77
- }
78
-
79
- let raw;
80
- if (typeof definition.getAuthRequirementsForStep === 'function') {
81
- raw = await definition.getAuthRequirementsForStep(step);
82
- } else {
83
- const module = this.createModule({
84
- userId,
85
- definition,
86
- ...(state ? { state } : {}),
87
- });
88
- raw = await singleStepRequirements(definition, module);
89
- }
90
-
91
- return {
92
- ...normalizeRequirements(raw),
93
- step,
94
- totalSteps,
95
- isMultiStep: totalSteps > 1,
96
- };
97
- }
98
- }
99
-
100
- module.exports = { GetAuthorizationRequirements, normalizeRequirements };
@@ -1,86 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
-
3
- /**
4
- * True when `user` may act on records owned by `ownerId`. Uses the User
5
- * domain object's rule (individual or organisation user model) when present.
6
- * @param {Object} user - authenticated User (or a plain { getId } double)
7
- * @param {string|number} ownerId
8
- */
9
- function userOwns(user, ownerId) {
10
- if (ownerId === undefined || ownerId === null || !user) return false;
11
- if (typeof user.ownsUserId === 'function') return user.ownsUserId(ownerId);
12
- return String(user.getId()) === String(ownerId);
13
- }
14
-
15
- const defaultCreateModule = (params) => {
16
- const { Module } = require('../module');
17
- return new Module(params);
18
- };
19
-
20
- /**
21
- * Loads an entity the caller owns and builds its Module (API client
22
- * hydrated from the persisted entity and credential). Management API v2.
23
- *
24
- * A missing entity and one owned by someone else are the same 404, so ids
25
- * cannot be probed (ADR-052 §3).
26
- */
27
- class GetEntityModuleForUser {
28
- /**
29
- * @param {Object} params
30
- * @param {import('../repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
31
- * @param {Array<Object>} params.moduleDefinitions
32
- * @param {Function} [params.createModule] - builds a Module; injectable for tests.
33
- */
34
- constructor({ moduleRepository, moduleDefinitions, createModule }) {
35
- this.moduleRepository = moduleRepository;
36
- this.moduleDefinitions = moduleDefinitions || [];
37
- this.createModule = createModule || defaultCreateModule;
38
- }
39
-
40
- /**
41
- * @param {string} entityId
42
- * @param {Object} user - authenticated User
43
- * @returns {Promise<{ entity: Object, definition: Object, module: Object }>}
44
- */
45
- async execute(entityId, user) {
46
- const entity = await this.loadOwnedEntity(entityId, user);
47
- const definition = this.moduleDefinitions.find(
48
- (def) => def.moduleName === entity.moduleName
49
- );
50
- if (!definition) {
51
- throw Boom.notFound(
52
- `Entity type ${entity.moduleName} is not configured in this app`,
53
- { code: 'ENTITY_TYPE_NOT_FOUND' }
54
- );
55
- }
56
- const module = this.createModule({
57
- userId: entity.userId,
58
- entity,
59
- definition,
60
- });
61
- return { entity, definition, module };
62
- }
63
-
64
- /** The entity record, or a 404 when it is missing or not the caller's. */
65
- async loadOwnedEntity(entityId, user) {
66
- let entity = null;
67
- try {
68
- entity = await this.moduleRepository.findEntityById(entityId);
69
- } catch (error) {
70
- // A malformed id (e.g. not an integer on Postgres) is "not found".
71
- if (/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
72
- entity = null;
73
- } else {
74
- throw error;
75
- }
76
- }
77
- if (!entity || !userOwns(user, entity.userId)) {
78
- throw Boom.notFound(`Entity ${entityId} not found`, {
79
- code: 'ENTITY_NOT_FOUND',
80
- });
81
- }
82
- return entity;
83
- }
84
- }
85
-
86
- module.exports = { GetEntityModuleForUser, userOwns };