@friggframework/schemas 2.0.0-next.78 → 2.0.0-next.80

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.
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Schema Validation Middleware for Express
3
+ *
4
+ * Provides request and response validation against JSON schemas using AJV.
5
+ * Like TypeScript for APIs - enforces contracts at runtime.
6
+ */
7
+
8
+ const Ajv = require('ajv');
9
+ const addFormats = require('ajv-formats');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // Initialize AJV with formats
14
+ const ajv = new Ajv({
15
+ allErrors: true,
16
+ verbose: true,
17
+ strict: false
18
+ // Note: coerceTypes disabled to avoid conflicts with oneOf schemas
19
+ });
20
+ addFormats(ajv);
21
+
22
+ // Load all schemas
23
+ const schemaDir = path.join(__dirname, '..', 'schemas');
24
+ const schemaFiles = [
25
+ 'api-entities.schema.json',
26
+ 'api-credentials.schema.json',
27
+ 'api-proxy.schema.json',
28
+ 'api-authorization.schema.json'
29
+ ];
30
+
31
+ schemaFiles.forEach(file => {
32
+ const schemaPath = path.join(schemaDir, file);
33
+ if (fs.existsSync(schemaPath)) {
34
+ const schemaContent = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
35
+ const schemaName = file.replace('.schema.json', '');
36
+ ajv.addSchema(schemaContent, schemaName);
37
+ }
38
+ });
39
+
40
+ /**
41
+ * Validation error class for schema validation failures
42
+ */
43
+ class SchemaValidationError extends Error {
44
+ constructor(message, errors, location) {
45
+ super(message);
46
+ this.name = 'SchemaValidationError';
47
+ this.errors = errors;
48
+ this.location = location; // 'body', 'query', 'params', 'response'
49
+ this.statusCode = location === 'response' ? 500 : 400;
50
+ }
51
+
52
+ toJSON() {
53
+ return {
54
+ error: 'ValidationError',
55
+ message: this.message,
56
+ location: this.location,
57
+ details: this.errors.map(err => ({
58
+ path: err.instancePath || 'root',
59
+ message: err.message,
60
+ params: err.params
61
+ }))
62
+ };
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Format AJV errors into human-readable messages
68
+ * @param {Array} errors - AJV validation errors
69
+ * @returns {string} - Formatted error message
70
+ */
71
+ function formatValidationErrors(errors) {
72
+ if (!errors || errors.length === 0) {
73
+ return 'Unknown validation error';
74
+ }
75
+
76
+ return errors.map(error => {
77
+ const path = error.instancePath || 'root';
78
+ const message = error.message;
79
+ const allowedValues = error.params?.allowedValues
80
+ ? ` (allowed: ${error.params.allowedValues.join(', ')})`
81
+ : '';
82
+ return `${path}: ${message}${allowedValues}`;
83
+ }).join('; ');
84
+ }
85
+
86
+ /**
87
+ * Get a compiled validator for a schema reference
88
+ * @param {string} schemaRef - Schema reference (e.g., 'api-entities#/definitions/entity')
89
+ * @returns {Function} - Compiled AJV validator
90
+ */
91
+ function getValidator(schemaRef) {
92
+ const validator = ajv.getSchema(schemaRef);
93
+ if (!validator) {
94
+ throw new Error(`Schema not found: ${schemaRef}`);
95
+ }
96
+ return validator;
97
+ }
98
+
99
+ /**
100
+ * Validate request body against a schema
101
+ * @param {string} schemaRef - Schema reference
102
+ * @param {Object} options - Validation options
103
+ * @param {boolean} options.strict - If true, throws on validation failure (default: true)
104
+ * @param {boolean} options.coerceTypes - If true, coerces types (default: false for body)
105
+ * @returns {Function} - Express middleware
106
+ */
107
+ function validateBody(schemaRef, options = {}) {
108
+ const { strict = true } = options;
109
+
110
+ return (req, res, next) => {
111
+ try {
112
+ const validator = getValidator(schemaRef);
113
+ const valid = validator(req.body);
114
+
115
+ if (!valid) {
116
+ const error = new SchemaValidationError(
117
+ `Request body validation failed: ${formatValidationErrors(validator.errors)}`,
118
+ validator.errors,
119
+ 'body'
120
+ );
121
+
122
+ if (strict) {
123
+ return res.status(400).json(error.toJSON());
124
+ }
125
+
126
+ // Non-strict: attach errors but continue
127
+ req.validationErrors = req.validationErrors || {};
128
+ req.validationErrors.body = validator.errors;
129
+ }
130
+
131
+ next();
132
+ } catch (err) {
133
+ next(err);
134
+ }
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Validate query parameters against a schema
140
+ * @param {string} schemaRef - Schema reference
141
+ * @param {Object} options - Validation options
142
+ * @returns {Function} - Express middleware
143
+ */
144
+ function validateQuery(schemaRef, options = {}) {
145
+ const { strict = true } = options;
146
+
147
+ return (req, res, next) => {
148
+ try {
149
+ const validator = getValidator(schemaRef);
150
+ const valid = validator(req.query);
151
+
152
+ if (!valid) {
153
+ const error = new SchemaValidationError(
154
+ `Query parameter validation failed: ${formatValidationErrors(validator.errors)}`,
155
+ validator.errors,
156
+ 'query'
157
+ );
158
+
159
+ if (strict) {
160
+ return res.status(400).json(error.toJSON());
161
+ }
162
+
163
+ req.validationErrors = req.validationErrors || {};
164
+ req.validationErrors.query = validator.errors;
165
+ }
166
+
167
+ next();
168
+ } catch (err) {
169
+ next(err);
170
+ }
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Validate route parameters against a schema
176
+ * @param {string} schemaRef - Schema reference
177
+ * @param {Object} options - Validation options
178
+ * @returns {Function} - Express middleware
179
+ */
180
+ function validateParams(schemaRef, options = {}) {
181
+ const { strict = true } = options;
182
+
183
+ return (req, res, next) => {
184
+ try {
185
+ const validator = getValidator(schemaRef);
186
+ const valid = validator(req.params);
187
+
188
+ if (!valid) {
189
+ const error = new SchemaValidationError(
190
+ `Route parameter validation failed: ${formatValidationErrors(validator.errors)}`,
191
+ validator.errors,
192
+ 'params'
193
+ );
194
+
195
+ if (strict) {
196
+ return res.status(400).json(error.toJSON());
197
+ }
198
+
199
+ req.validationErrors = req.validationErrors || {};
200
+ req.validationErrors.params = validator.errors;
201
+ }
202
+
203
+ next();
204
+ } catch (err) {
205
+ next(err);
206
+ }
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Validate response body against a schema (for development/testing)
212
+ * Wraps res.json() to validate response data
213
+ * @param {string} schemaRef - Schema reference
214
+ * @param {Object} options - Validation options
215
+ * @returns {Function} - Express middleware
216
+ */
217
+ function validateResponse(schemaRef, options = {}) {
218
+ const { strict = false, logErrors = true } = options;
219
+
220
+ return (req, res, next) => {
221
+ const originalJson = res.json.bind(res);
222
+
223
+ res.json = function(data) {
224
+ try {
225
+ const validator = getValidator(schemaRef);
226
+ const valid = validator(data);
227
+
228
+ if (!valid) {
229
+ const errorMessage = formatValidationErrors(validator.errors);
230
+
231
+ if (logErrors) {
232
+ console.error(`Response validation failed for ${req.method} ${req.path}:`, errorMessage);
233
+ }
234
+
235
+ if (strict) {
236
+ const error = new SchemaValidationError(
237
+ `Response validation failed: ${errorMessage}`,
238
+ validator.errors,
239
+ 'response'
240
+ );
241
+ return originalJson.call(res.status(500), error.toJSON());
242
+ }
243
+
244
+ // Non-strict: log but continue
245
+ // Optionally add validation metadata to response
246
+ if (options.includeMetadata) {
247
+ data._validation = {
248
+ valid: false,
249
+ errors: validator.errors
250
+ };
251
+ }
252
+ }
253
+ } catch (err) {
254
+ if (logErrors) {
255
+ console.error('Response validation error:', err.message);
256
+ }
257
+ }
258
+
259
+ return originalJson.call(res, data);
260
+ };
261
+
262
+ next();
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Combined validation middleware - validates request and response
268
+ * @param {Object} config - Validation configuration
269
+ * @param {string} config.body - Schema ref for body validation
270
+ * @param {string} config.query - Schema ref for query validation
271
+ * @param {string} config.params - Schema ref for params validation
272
+ * @param {string} config.response - Schema ref for response validation
273
+ * @param {Object} config.options - Validation options
274
+ * @returns {Array<Function>} - Array of Express middleware
275
+ */
276
+ function validate(config = {}) {
277
+ const middlewares = [];
278
+ const { options = {} } = config;
279
+
280
+ if (config.body) {
281
+ middlewares.push(validateBody(config.body, options));
282
+ }
283
+
284
+ if (config.query) {
285
+ middlewares.push(validateQuery(config.query, options));
286
+ }
287
+
288
+ if (config.params) {
289
+ middlewares.push(validateParams(config.params, options));
290
+ }
291
+
292
+ if (config.response) {
293
+ middlewares.push(validateResponse(config.response, options));
294
+ }
295
+
296
+ return middlewares;
297
+ }
298
+
299
+ /**
300
+ * Schema reference helpers for common API schemas
301
+ */
302
+ const SchemaRefs = {
303
+ // Entities
304
+ entity: 'api-entities#/definitions/entity',
305
+ listEntitiesResponse: 'api-entities#/definitions/listEntitiesResponse',
306
+ createEntityRequest: 'api-entities#/definitions/createEntityRequest',
307
+ createEntityResponse: 'api-entities#/definitions/createEntityResponse',
308
+ entityType: 'api-entities#/definitions/entityType',
309
+ listEntityTypesResponse: 'api-entities#/definitions/listEntityTypesResponse',
310
+ getEntityTypeResponse: 'api-entities#/definitions/getEntityTypeResponse',
311
+ reauthorizeEntityRequest: 'api-entities#/definitions/reauthorizeEntityRequest',
312
+ reauthorizeEntityResponse: 'api-entities#/definitions/reauthorizeEntityResponse',
313
+
314
+ // Credentials
315
+ credential: 'api-credentials#/definitions/credential',
316
+ listCredentialsResponse: 'api-credentials#/definitions/listCredentialsResponse',
317
+ getCredentialResponse: 'api-credentials#/definitions/getCredentialResponse',
318
+ deleteCredentialResponse: 'api-credentials#/definitions/deleteCredentialResponse',
319
+ reauthorizeCredentialRequest: 'api-credentials#/definitions/reauthorizeCredentialRequest',
320
+ reauthorizeCredentialResponse: 'api-credentials#/definitions/reauthorizeCredentialResponse',
321
+
322
+ // Proxy
323
+ proxyRequest: 'api-proxy#/definitions/proxyRequest',
324
+ proxyResponse: 'api-proxy#/definitions/proxyResponse',
325
+ proxyErrorResponse: 'api-proxy#/definitions/proxyErrorResponse',
326
+ proxyResponseUnion: 'api-proxy#/definitions/proxyResponseUnion',
327
+
328
+ // Authorization
329
+ authorizationRequirements: 'api-authorization#/definitions/authorizationRequirements',
330
+ authorizationRequest: 'api-authorization#/definitions/authorizationRequest',
331
+ authorizationResponse: 'api-authorization#/definitions/authorizationResponse',
332
+ authorizationSession: 'api-authorization#/definitions/authorizationSession',
333
+ getEntityTypeRequirementsResponse: 'api-authorization#/definitions/getEntityTypeRequirementsResponse'
334
+ };
335
+
336
+ /**
337
+ * Precompiled validators for performance
338
+ */
339
+ const Validators = {};
340
+
341
+ // Lazy-load validators on first use
342
+ function getCompiledValidator(name) {
343
+ if (!Validators[name]) {
344
+ const ref = SchemaRefs[name];
345
+ if (!ref) {
346
+ throw new Error(`Unknown schema name: ${name}`);
347
+ }
348
+ Validators[name] = getValidator(ref);
349
+ }
350
+ return Validators[name];
351
+ }
352
+
353
+ /**
354
+ * Direct validation functions (not middleware)
355
+ * Useful for testing and manual validation
356
+ */
357
+ function validateData(schemaName, data) {
358
+ const validator = getCompiledValidator(schemaName);
359
+ const valid = validator(data);
360
+ return {
361
+ valid,
362
+ errors: valid ? null : validator.errors,
363
+ formatted: valid ? null : formatValidationErrors(validator.errors)
364
+ };
365
+ }
366
+
367
+ module.exports = {
368
+ // Middleware
369
+ validateBody,
370
+ validateQuery,
371
+ validateParams,
372
+ validateResponse,
373
+ validate,
374
+
375
+ // Direct validation
376
+ validateData,
377
+ getValidator,
378
+ formatValidationErrors,
379
+
380
+ // Schema references
381
+ SchemaRefs,
382
+
383
+ // Error class
384
+ SchemaValidationError,
385
+
386
+ // AJV instance (for advanced use)
387
+ ajv
388
+ };
@@ -0,0 +1,280 @@
1
+ # Authorization Mocks
2
+
3
+ Schema-compliant mock data generators for testing authentication and authorization flows across the Frigg Framework.
4
+
5
+ ## Purpose
6
+
7
+ These mocks ensure consistency across all Frigg packages:
8
+ - `@friggframework/core` - Backend authorization logic
9
+ - `@friggframework/ui` - Frontend integration components
10
+ - `@friggframework/devtools/management-ui` - Developer tooling
11
+
12
+ All mock data is **validated against canonical JSON schemas** to guarantee accuracy.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @friggframework/schemas
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Basic Examples
23
+
24
+ ```javascript
25
+ const {
26
+ createOAuth2Requirements,
27
+ createFormRequirements,
28
+ createNagarisOTPFlowMock,
29
+ createAuthorizationSuccess,
30
+ } = require('@friggframework/schemas/mocks/authorization-mocks');
31
+
32
+ // OAuth2 flow
33
+ const hubspotAuth = createOAuth2Requirements('hubspot');
34
+ // {
35
+ // type: 'oauth2',
36
+ // step: 1,
37
+ // totalSteps: 1,
38
+ // isMultiStep: false,
39
+ // data: {
40
+ // url: 'https://auth.hubspot.com/oauth/authorize?...',
41
+ // scopes: ['read', 'write']
42
+ // }
43
+ // }
44
+
45
+ // Form-based auth
46
+ const apiKeyAuth = createFormRequirements('custom-api', {
47
+ fields: ['api_key']
48
+ });
49
+ // {
50
+ // type: 'form',
51
+ // data: {
52
+ // jsonSchema: { ... },
53
+ // uiSchema: { ... }
54
+ // }
55
+ // }
56
+
57
+ // Multi-step OTP flow (like Nagaris)
58
+ const nagarisFlow = createNagarisOTPFlowMock('user-123');
59
+ const step1Reqs = nagarisFlow.getStep1Requirements(); // Email form
60
+ const step1Response = nagarisFlow.submitStep1({ email: 'test@example.com' }); // OTP sent
61
+ const step2Response = nagarisFlow.submitStep2({ otp: '123456' }); // Success
62
+ ```
63
+
64
+ ### In Tests
65
+
66
+ #### Core Package Tests
67
+
68
+ ```javascript
69
+ // packages/core/__tests__/authorization-flow.test.js
70
+ const { createNagarisOTPFlowMock } = require('@friggframework/schemas/mocks/authorization-mocks');
71
+ const { validateAuthorizationSession } = require('@friggframework/schemas');
72
+
73
+ test('processes multi-step auth', async () => {
74
+ const mockFlow = createNagarisOTPFlowMock('user-123');
75
+ const session = mockFlow.session;
76
+
77
+ // Validate before storing
78
+ const validation = validateAuthorizationSession(session);
79
+ expect(validation.valid).toBe(true);
80
+
81
+ // Use in repository test
82
+ await authSessionRepository.create(session);
83
+ });
84
+ ```
85
+
86
+ #### UI Package Tests
87
+
88
+ ```javascript
89
+ // packages/ui/__tests__/AuthorizationWizard.test.jsx
90
+ const { createFormRequirements } = require('@friggframework/schemas/mocks/authorization-mocks');
91
+
92
+ test('renders multi-step OTP form', async () => {
93
+ const mockApi = {
94
+ getAuthorizationRequirements: jest.fn().mockResolvedValue(
95
+ createFormRequirements('nagaris', {
96
+ fields: ['email'],
97
+ isMultiStep: true,
98
+ step: 1,
99
+ totalSteps: 2
100
+ })
101
+ )
102
+ };
103
+
104
+ render(<AuthorizationWizard api={mockApi} moduleType="nagaris" />);
105
+ // Test form rendering...
106
+ });
107
+ ```
108
+
109
+ #### Management UI Tests
110
+
111
+ ```javascript
112
+ // packages/devtools/management-ui/__tests__/TestingZone.test.jsx
113
+ const { createOAuth2Requirements } = require('@friggframework/schemas/mocks/authorization-mocks');
114
+
115
+ test('displays OAuth authorization URL', () => {
116
+ const mockData = createOAuth2Requirements('hubspot');
117
+
118
+ render(<AuthFlowDisplay requirements={mockData} />);
119
+ expect(screen.getByText(/hubspot.com\/oauth/)).toBeInTheDocument();
120
+ });
121
+ ```
122
+
123
+ ## API Reference
124
+
125
+ ### OAuth2 Flows
126
+
127
+ #### `createOAuth2Requirements(moduleType, options)`
128
+
129
+ Create OAuth2 authorization requirements.
130
+
131
+ **Parameters:**
132
+ - `moduleType` (string): Module name (e.g., 'hubspot', 'salesforce')
133
+ - `options.scopes` (array): OAuth scopes (default: ['read', 'write'])
134
+ - `options.isMultiStep` (boolean): Multi-step flow (default: false)
135
+ - `options.step` (number): Current step (default: 1)
136
+ - `options.totalSteps` (number): Total steps (default: 1)
137
+ - `options.sessionId` (string): Session ID for multi-step
138
+
139
+ **Returns:** Authorization requirements object (validated against schema)
140
+
141
+ #### `createOAuth2FlowMock(moduleType, userId)`
142
+
143
+ Create complete OAuth2 flow with methods for each step.
144
+
145
+ **Returns:** Object with `getRequirements()` and `handleCallback()` methods
146
+
147
+ ### Form-Based Flows
148
+
149
+ #### `createFormRequirements(moduleType, options)`
150
+
151
+ Create form-based authorization requirements with JSON Schema.
152
+
153
+ **Parameters:**
154
+ - `moduleType` (string): Module name
155
+ - `options.fields` (array): Field names (email, password, api_key, otp, etc.)
156
+ - `options.isMultiStep` (boolean): Multi-step flow
157
+ - `options.step` (number): Current step
158
+ - `options.totalSteps` (number): Total steps
159
+ - `options.sessionId` (string): Session ID
160
+
161
+ **Returns:** Form requirements with jsonSchema and uiSchema
162
+
163
+ **Supported Fields:**
164
+ - `email` - Email input with validation
165
+ - `password` - Password input (min 6 chars)
166
+ - `api_key` - API key text input
167
+ - `otp` - 6-digit OTP input with pattern validation
168
+ - Custom fields - Generic text inputs
169
+
170
+ ### Multi-Step OTP Flows
171
+
172
+ #### `createOTPMultiStepFlow(moduleType)`
173
+
174
+ Create multi-step flow structure (email → OTP).
175
+
176
+ **Returns:** Object with `step1` and `step2(sessionId)` properties
177
+
178
+ #### `createNagarisOTPFlowMock(userId)`
179
+
180
+ Create complete Nagaris-style OTP flow with all steps.
181
+
182
+ **Returns:** Object with methods:
183
+ - `getStep1Requirements()` - Get email form
184
+ - `submitStep1(emailData)` - Submit email, get OTP prompt
185
+ - `submitStep2(otpData)` - Submit OTP, get success
186
+ - `session` - Authorization session object
187
+ - `sessionId` - Session identifier
188
+ - `email` - Test email address
189
+
190
+ ### Response Builders
191
+
192
+ #### `createAuthorizationSuccess(moduleType, options)`
193
+
194
+ Create successful authorization response.
195
+
196
+ **Parameters:**
197
+ - `moduleType` (string): Module name
198
+ - `options.entityId` (string): Entity ID (auto-generated if not provided)
199
+ - `options.credentialId` (string): Credential ID (auto-generated)
200
+ - `options.display` (string): Display name
201
+
202
+ **Returns:** Success response object
203
+
204
+ #### `createAuthorizationNextStep(nextStep, requirements, options)`
205
+
206
+ Create next step response for multi-step flows.
207
+
208
+ **Parameters:**
209
+ - `nextStep` (number): Next step number
210
+ - `requirements` (object): Requirements for next step
211
+ - `options.sessionId` (string): Session ID (auto-generated)
212
+ - `options.message` (string): User message
213
+
214
+ **Returns:** Next step response object
215
+
216
+ ### Session Management
217
+
218
+ #### `createAuthorizationSession(userId, entityType, options)`
219
+
220
+ Create authorization session database object.
221
+
222
+ **Parameters:**
223
+ - `userId` (string): User ID
224
+ - `entityType` (string): Module type
225
+ - `options.currentStep` (number): Current step (default: 1)
226
+ - `options.maxSteps` (number): Total steps (default: 2)
227
+ - `options.stepData` (object): Data from previous steps
228
+ - `options.expiresInMinutes` (number): Expiration time (default: 15)
229
+ - `options.completed` (boolean): Completion status
230
+
231
+ **Returns:** Session object ready for database storage
232
+
233
+ #### `generateSessionId()`
234
+
235
+ Generate a UUID v4 session ID.
236
+
237
+ **Returns:** UUID string
238
+
239
+ ## Validation
240
+
241
+ All mock data is validated against schemas in `packages/schemas/schemas/api-authorization.schema.json`.
242
+
243
+ ```javascript
244
+ const { validateAuthorizationRequirements } = require('@friggframework/schemas');
245
+
246
+ const mockData = createFormRequirements('nagaris', { fields: ['email'] });
247
+ const result = validateAuthorizationRequirements(mockData);
248
+
249
+ if (result.valid) {
250
+ console.log('✅ Mock data is schema-compliant');
251
+ } else {
252
+ console.error('❌ Validation errors:', result.errors);
253
+ }
254
+ ```
255
+
256
+ ## Testing
257
+
258
+ Run the mock validation tests:
259
+
260
+ ```bash
261
+ cd packages/schemas
262
+ npm test mocks/__tests__/authorization-mocks.test.js
263
+ ```
264
+
265
+ All tests validate that mocks are schema-compliant and work across packages.
266
+
267
+ ## Contributing
268
+
269
+ When adding new authorization types:
270
+
271
+ 1. Add schema definition to `api-authorization.schema.json`
272
+ 2. Add mock generator to `authorization-mocks.js`
273
+ 3. Add validation tests to `__tests__/authorization-mocks.test.js`
274
+ 4. Update this README with usage examples
275
+
276
+ ## Related
277
+
278
+ - [API Authorization Schema](../schemas/api-authorization.schema.json)
279
+ - [Core Authorization Use Cases](../../core/modules/use-cases/)
280
+ - [UI Authorization Components](../../ui/lib/integration/presentation/components/)