@friggframework/devtools 2.0.0-next.63 ā 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.
- package/frigg-cli/auth-command/CLAUDE.md +293 -0
- package/frigg-cli/auth-command/README.md +450 -0
- package/frigg-cli/auth-command/api-key-flow.js +153 -0
- package/frigg-cli/auth-command/auth-tester.js +344 -0
- package/frigg-cli/auth-command/credential-storage.js +182 -0
- package/frigg-cli/auth-command/index.js +256 -0
- package/frigg-cli/auth-command/json-schema-form.js +67 -0
- package/frigg-cli/auth-command/module-loader.js +172 -0
- package/frigg-cli/auth-command/oauth-callback-server.js +431 -0
- package/frigg-cli/auth-command/oauth-flow.js +195 -0
- package/frigg-cli/auth-command/utils/browser.js +30 -0
- package/frigg-cli/index.js +36 -1
- package/package.json +8 -7
- package/test/auther-definition-method-tester.js +45 -0
- package/test/index.js +9 -0
- package/test/integration-validator.js +2 -0
- package/test/mock-api-readme.md +102 -0
- package/test/mock-api.js +282 -0
- package/test/mock-integration.js +78 -0
|
@@ -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 };
|