@friggframework/devtools 2.0.0-next.64 → 2.0.0-next.65

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,256 @@
1
+ const chalk = require('chalk');
2
+ const { loadModule, validateModule, getAuthType } = require('./module-loader');
3
+ const { runOAuthFlow } = require('./oauth-flow');
4
+ const { runApiKeyFlow } = require('./api-key-flow');
5
+ const { CredentialStorage } = require('./credential-storage');
6
+ const { runAuthTests } = require('./auth-tester');
7
+
8
+ async function test(moduleName, options) {
9
+ console.log(chalk.blue.bold(`\nšŸ” Frigg Authenticator\n`));
10
+ console.log(chalk.gray(`Testing authentication for: ${moduleName}\n`));
11
+
12
+ try {
13
+ // 1. Load and validate module
14
+ const { definition, Api } = await loadModule(moduleName);
15
+ validateModule(definition);
16
+
17
+ // 2. Determine auth type
18
+ const authType = getAuthType(Api);
19
+ console.log(chalk.gray(`Auth type detected: ${authType}`));
20
+
21
+ let credentials;
22
+
23
+ // 3. Run appropriate auth flow
24
+ if (authType === 'apiKey' || authType === 'api_key') {
25
+ // API key flow handles missing --api-key by checking for getAuthorizationRequirements
26
+ // and rendering an interactive form if available
27
+ credentials = await runApiKeyFlow(definition, Api, options.apiKey, options);
28
+ } else {
29
+ // OAuth2 flow
30
+ credentials = await runOAuthFlow(definition, Api, {
31
+ port: parseInt(options.port, 10) || 3333,
32
+ timeout: parseInt(options.timeout, 10) || 300,
33
+ verbose: options.verbose,
34
+ });
35
+ }
36
+
37
+ // 4. Run verification tests
38
+ await runAuthTests(definition, Api, credentials, {
39
+ verbose: options.verbose,
40
+ });
41
+
42
+ // 5. Save credentials using actual module name from definition
43
+ const actualModuleName = definition.moduleName || definition.getName?.() || moduleName;
44
+ const storage = new CredentialStorage();
45
+ const savedPath = await storage.save(actualModuleName, credentials, authType);
46
+
47
+ console.log(chalk.green(`\nāœ“ Authentication successful for ${actualModuleName}!`));
48
+ console.log(chalk.gray(` Credentials saved to: ${savedPath}`));
49
+ console.log(chalk.gray(`\n Use 'frigg auth get ${actualModuleName} --json' to retrieve credentials.\n`));
50
+
51
+ } catch (error) {
52
+ console.log(chalk.red(`\nāœ— Authentication failed: ${error.message}`));
53
+ if (options.verbose && error.stack) {
54
+ console.log(chalk.gray('\nStack trace:'));
55
+ console.log(chalk.gray(error.stack));
56
+ }
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ async function list(options) {
62
+ const storage = new CredentialStorage();
63
+ const credentials = await storage.list();
64
+
65
+ if (options.json) {
66
+ console.log(JSON.stringify(credentials, null, 2));
67
+ return;
68
+ }
69
+
70
+ console.log(chalk.blue.bold('\nšŸ” Saved Credentials\n'));
71
+
72
+ if (credentials.length === 0) {
73
+ console.log(chalk.gray(' No credentials saved.\n'));
74
+ console.log(chalk.gray(' Run `frigg auth test <module>` to authenticate a module.\n'));
75
+ return;
76
+ }
77
+
78
+ // Display as table
79
+ const tableData = credentials.map(c => ({
80
+ Module: c.module,
81
+ 'Auth Type': c.authType,
82
+ Entity: c.entity,
83
+ 'Has Access Token': c.hasAccessToken ? 'āœ“' : 'āœ—',
84
+ 'Has Refresh Token': c.hasRefreshToken ? 'āœ“' : '-',
85
+ 'Saved At': formatDate(c.savedAt),
86
+ }));
87
+
88
+ console.table(tableData);
89
+ console.log('');
90
+ }
91
+
92
+ async function get(moduleName, options) {
93
+ const storage = new CredentialStorage();
94
+ const credentials = await storage.get(moduleName);
95
+
96
+ if (!credentials) {
97
+ console.log(chalk.red(`\nāœ— No credentials found for: ${moduleName}`));
98
+ console.log(chalk.gray(`\n Run 'frigg auth test ${moduleName}' to authenticate.\n`));
99
+ process.exit(1);
100
+ }
101
+
102
+ if (options.json) {
103
+ console.log(JSON.stringify(credentials, null, 2));
104
+ return;
105
+ }
106
+
107
+ if (options.export) {
108
+ outputAsEnvVars(moduleName, credentials);
109
+ return;
110
+ }
111
+
112
+ // Display formatted
113
+ console.log(chalk.blue.bold(`\nšŸ” Credentials for ${moduleName}\n`));
114
+
115
+ console.log(chalk.gray('Auth Type:'), credentials.authType);
116
+ console.log(chalk.gray('Obtained:'), formatDate(credentials.obtainedAt));
117
+ console.log(chalk.gray('Saved:'), formatDate(credentials.savedAt));
118
+
119
+ if (credentials.entity?.details?.name) {
120
+ console.log(chalk.gray('Entity:'), credentials.entity.details.name);
121
+ }
122
+ if (credentials.entity?.identifiers?.externalId) {
123
+ console.log(chalk.gray('External ID:'), credentials.entity.identifiers.externalId);
124
+ }
125
+
126
+ console.log(chalk.gray('\nTokens:'));
127
+ if (credentials.tokens?.access_token) {
128
+ console.log(chalk.gray(' access_token:'), maskToken(credentials.tokens.access_token));
129
+ }
130
+ if (credentials.tokens?.refresh_token) {
131
+ console.log(chalk.gray(' refresh_token:'), maskToken(credentials.tokens.refresh_token));
132
+ }
133
+ if (credentials.apiKey) {
134
+ console.log(chalk.gray(' api_key:'), maskToken(credentials.apiKey));
135
+ }
136
+
137
+ console.log(chalk.gray(`\n Use --json for full output or --export for environment variables.\n`));
138
+ }
139
+
140
+ async function deleteCredentials(moduleName, options) {
141
+ const storage = new CredentialStorage();
142
+
143
+ if (options.all) {
144
+ if (!options.yes) {
145
+ const confirmed = await confirmAction('Delete ALL saved credentials?');
146
+ if (!confirmed) {
147
+ console.log(chalk.gray('Cancelled.'));
148
+ return;
149
+ }
150
+ }
151
+ await storage.deleteAll();
152
+ console.log(chalk.green('āœ“ All credentials deleted'));
153
+ return;
154
+ }
155
+
156
+ if (!moduleName) {
157
+ console.log(chalk.red('āœ— Error: Please specify a module name or use --all'));
158
+ console.log(chalk.gray('\nUsage:'));
159
+ console.log(chalk.gray(' frigg auth delete <module>'));
160
+ console.log(chalk.gray(' frigg auth delete --all'));
161
+ process.exit(1);
162
+ }
163
+
164
+ const exists = await storage.get(moduleName);
165
+ if (!exists) {
166
+ console.log(chalk.yellow(`No credentials found for: ${moduleName}`));
167
+ return;
168
+ }
169
+
170
+ if (!options.yes) {
171
+ const confirmed = await confirmAction(`Delete credentials for ${moduleName}?`);
172
+ if (!confirmed) {
173
+ console.log(chalk.gray('Cancelled.'));
174
+ return;
175
+ }
176
+ }
177
+
178
+ const deleted = await storage.delete(moduleName);
179
+ if (deleted) {
180
+ console.log(chalk.green(`āœ“ Credentials deleted for ${moduleName}`));
181
+ } else {
182
+ console.log(chalk.yellow(`No credentials found for: ${moduleName}`));
183
+ }
184
+ }
185
+
186
+ // Helper functions
187
+
188
+ function formatDate(dateStr) {
189
+ if (!dateStr) return 'Unknown';
190
+ try {
191
+ const date = new Date(dateStr);
192
+ return date.toLocaleString();
193
+ } catch {
194
+ return dateStr;
195
+ }
196
+ }
197
+
198
+ function maskToken(token) {
199
+ if (!token || token.length <= 8) {
200
+ return '***';
201
+ }
202
+ return token.slice(0, 4) + '...' + token.slice(-4);
203
+ }
204
+
205
+ function outputAsEnvVars(moduleName, credentials) {
206
+ const prefix = moduleName.toUpperCase().replace(/-/g, '_');
207
+
208
+ const vars = [];
209
+
210
+ if (credentials.tokens?.access_token) {
211
+ vars.push(`export ${prefix}_ACCESS_TOKEN="${credentials.tokens.access_token}"`);
212
+ }
213
+ if (credentials.tokens?.refresh_token) {
214
+ vars.push(`export ${prefix}_REFRESH_TOKEN="${credentials.tokens.refresh_token}"`);
215
+ }
216
+ if (credentials.apiKey) {
217
+ vars.push(`export ${prefix}_API_KEY="${credentials.apiKey}"`);
218
+ }
219
+ if (credentials.entity?.identifiers?.externalId) {
220
+ vars.push(`export ${prefix}_EXTERNAL_ID="${credentials.entity.identifiers.externalId}"`);
221
+ }
222
+ if (credentials.apiParams?.companyDomain) {
223
+ vars.push(`export ${prefix}_COMPANY_DOMAIN="${credentials.apiParams.companyDomain}"`);
224
+ }
225
+
226
+ if (vars.length === 0) {
227
+ console.log(chalk.yellow('# No credentials to export'));
228
+ } else {
229
+ console.log(vars.join('\n'));
230
+ }
231
+ }
232
+
233
+ async function confirmAction(message) {
234
+ // Simple confirmation using readline
235
+ const readline = require('readline');
236
+ const rl = readline.createInterface({
237
+ input: process.stdin,
238
+ output: process.stdout,
239
+ });
240
+
241
+ return new Promise((resolve) => {
242
+ rl.question(chalk.yellow(`${message} [y/N] `), (answer) => {
243
+ rl.close();
244
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
245
+ });
246
+ });
247
+ }
248
+
249
+ module.exports = {
250
+ authCommand: {
251
+ test,
252
+ list,
253
+ get,
254
+ delete: deleteCredentials,
255
+ },
256
+ };
@@ -0,0 +1,67 @@
1
+ const { input, password } = require('@inquirer/prompts');
2
+ const chalk = require('chalk');
3
+
4
+ /**
5
+ * Renders a JSON Schema form as interactive CLI prompts
6
+ *
7
+ * @param {Object} jsonSchema - JSON Schema object with properties, required fields, etc.
8
+ * @param {Object} uiSchema - UI Schema with rendering hints (ui:widget, ui:help, ui:placeholder)
9
+ * @returns {Promise<Object>} - Object containing all form field values
10
+ */
11
+ async function renderJsonSchemaForm(jsonSchema, uiSchema = {}) {
12
+ const results = {};
13
+ const properties = jsonSchema.properties || {};
14
+ const required = jsonSchema.required || [];
15
+
16
+ // Display form title
17
+ if (jsonSchema.title) {
18
+ console.log(chalk.blue(`\nšŸ“ ${jsonSchema.title}\n`));
19
+ }
20
+
21
+ for (const [key, prop] of Object.entries(properties)) {
22
+ const ui = uiSchema[key] || {};
23
+ const isRequired = required.includes(key);
24
+ const isPassword = ui['ui:widget'] === 'password';
25
+
26
+ // Build prompt message with title
27
+ const fieldTitle = prop.title || key;
28
+
29
+ // Show help text before the prompt if available
30
+ if (ui['ui:help']) {
31
+ console.log(chalk.gray(` (${ui['ui:help']})`));
32
+ }
33
+
34
+ let value;
35
+ if (isPassword) {
36
+ value = await password({
37
+ message: fieldTitle,
38
+ mask: '*',
39
+ validate: (input) => {
40
+ if (isRequired && !input) {
41
+ return `${fieldTitle} is required`;
42
+ }
43
+ return true;
44
+ },
45
+ });
46
+ } else {
47
+ value = await input({
48
+ message: fieldTitle,
49
+ default: '',
50
+ validate: (input) => {
51
+ if (isRequired && !input) {
52
+ return `${fieldTitle} is required`;
53
+ }
54
+ return true;
55
+ },
56
+ });
57
+ }
58
+
59
+ if (value) {
60
+ results[key] = value;
61
+ }
62
+ }
63
+
64
+ return results;
65
+ }
66
+
67
+ module.exports = { renderJsonSchemaForm };
@@ -0,0 +1,172 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const chalk = require('chalk');
4
+
5
+ async function loadModule(moduleIdentifier) {
6
+ let modulePath;
7
+ let searchedPaths = [];
8
+
9
+ // 1. Check if it's a relative/absolute path
10
+ if (moduleIdentifier.startsWith('.') || moduleIdentifier.startsWith('/')) {
11
+ modulePath = path.resolve(process.cwd(), moduleIdentifier);
12
+ if (!fs.existsSync(modulePath)) {
13
+ throw new Error(`Module path not found: ${modulePath}`);
14
+ }
15
+ }
16
+ // 2. Check if it's a scoped package name
17
+ else if (moduleIdentifier.startsWith('@')) {
18
+ modulePath = resolveFromNodeModules(moduleIdentifier, searchedPaths);
19
+ if (!modulePath) {
20
+ throw new Error(
21
+ `Could not find module: ${moduleIdentifier}\nSearched paths:\n${searchedPaths.map(p => ` - ${p}`).join('\n')}`
22
+ );
23
+ }
24
+ }
25
+ // 3. Assume it's a short name like "attio" -> "@friggframework/api-module-attio"
26
+ else {
27
+ const fullName = `@friggframework/api-module-${moduleIdentifier}`;
28
+ modulePath = resolveFromNodeModules(fullName, searchedPaths);
29
+
30
+ // If not found, try local src/api-modules path
31
+ if (!modulePath) {
32
+ const localPath = path.join(process.cwd(), 'src', 'api-modules', moduleIdentifier);
33
+ searchedPaths.push(localPath);
34
+ if (fs.existsSync(localPath)) {
35
+ modulePath = localPath;
36
+ }
37
+ }
38
+
39
+ // Also try backend/src/api-modules (common Frigg structure)
40
+ if (!modulePath) {
41
+ const backendLocalPath = path.join(process.cwd(), 'backend', 'src', 'api-modules', moduleIdentifier);
42
+ searchedPaths.push(backendLocalPath);
43
+ if (fs.existsSync(backendLocalPath)) {
44
+ modulePath = backendLocalPath;
45
+ }
46
+ }
47
+
48
+ if (!modulePath) {
49
+ throw new Error(
50
+ `Could not find module: ${moduleIdentifier}\nTried:\n` +
51
+ ` - ${fullName}\n` +
52
+ `Searched paths:\n${searchedPaths.map(p => ` - ${p}`).join('\n')}`
53
+ );
54
+ }
55
+ }
56
+
57
+ console.log(chalk.gray(`Loading module from: ${modulePath}`));
58
+
59
+ // Load the module
60
+ let moduleExports;
61
+ try {
62
+ moduleExports = require(modulePath);
63
+ } catch (err) {
64
+ throw new Error(`Failed to load module from ${modulePath}: ${err.message}`);
65
+ }
66
+
67
+ // Extract Definition and Api
68
+ const definition = moduleExports.Definition || moduleExports.definition;
69
+ const Api = definition?.API || moduleExports.Api || moduleExports.API;
70
+
71
+ if (!definition) {
72
+ throw new Error(
73
+ `Module ${moduleIdentifier} does not export a Definition.\n` +
74
+ `Expected exports: { Definition } or { definition }`
75
+ );
76
+ }
77
+
78
+ if (!Api) {
79
+ throw new Error(
80
+ `Module ${moduleIdentifier} does not export an API class.\n` +
81
+ `Expected Definition.API or exports.Api`
82
+ );
83
+ }
84
+
85
+ return { definition, Api, modulePath };
86
+ }
87
+
88
+ function resolveFromNodeModules(packageName, searchedPaths = []) {
89
+ const pathsToCheck = [
90
+ // Current working directory
91
+ path.join(process.cwd(), 'node_modules', packageName),
92
+ // Backend subdirectory (common Frigg structure)
93
+ path.join(process.cwd(), 'backend', 'node_modules', packageName),
94
+ // Parent directory (for monorepos)
95
+ path.join(process.cwd(), '..', 'node_modules', packageName),
96
+ // Global installation location (via npm root -g)
97
+ path.join(process.execPath, '..', '..', 'lib', 'node_modules', packageName),
98
+ ];
99
+
100
+ for (const checkPath of pathsToCheck) {
101
+ searchedPaths.push(checkPath);
102
+ if (fs.existsSync(checkPath)) {
103
+ return checkPath;
104
+ }
105
+ }
106
+
107
+ return null;
108
+ }
109
+
110
+ function validateModule(definition) {
111
+ const errors = [];
112
+
113
+ // Check required fields
114
+ const requiredFields = ['moduleName', 'API'];
115
+ for (const field of requiredFields) {
116
+ if (!definition[field]) {
117
+ errors.push(`Missing required field: ${field}`);
118
+ }
119
+ }
120
+
121
+ // Check requiredAuthMethods
122
+ if (!definition.requiredAuthMethods) {
123
+ errors.push('Missing requiredAuthMethods object');
124
+ } else {
125
+ const requiredMethods = ['getEntityDetails', 'testAuthRequest', 'apiPropertiesToPersist'];
126
+ for (const method of requiredMethods) {
127
+ if (!definition.requiredAuthMethods[method]) {
128
+ errors.push(`Missing required auth method: ${method}`);
129
+ }
130
+ }
131
+
132
+ // getToken is required for OAuth2, optional for API-Key
133
+ // getCredentialDetails is recommended but not strictly required
134
+ }
135
+
136
+ if (errors.length > 0) {
137
+ throw new Error(
138
+ `Module validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`
139
+ );
140
+ }
141
+
142
+ console.log(chalk.green(`āœ“ Module validation passed: ${definition.moduleName}`));
143
+ }
144
+
145
+ function getAuthType(Api) {
146
+ // Check the requesterType static property
147
+ if (Api.requesterType) {
148
+ return Api.requesterType;
149
+ }
150
+
151
+ // Check prototype chain for OAuth2Requester or ApiKeyRequester
152
+ const className = Api.name;
153
+ const protoChain = [];
154
+ let proto = Api;
155
+ while (proto && proto.name) {
156
+ protoChain.push(proto.name);
157
+ proto = Object.getPrototypeOf(proto);
158
+ }
159
+
160
+ if (protoChain.some(name => name.includes('OAuth2') || name.includes('Oauth2'))) {
161
+ return 'oauth2';
162
+ }
163
+
164
+ if (protoChain.some(name => name.includes('ApiKey') || name.includes('APIKey'))) {
165
+ return 'apiKey';
166
+ }
167
+
168
+ // Default to oauth2
169
+ return 'oauth2';
170
+ }
171
+
172
+ module.exports = { loadModule, validateModule, getAuthType };