@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,344 @@
1
+ const chalk = require('chalk');
2
+
3
+ async function runAuthTests(definition, ApiClass, credentials, options) {
4
+ console.log(chalk.blue('\n๐Ÿงช Running Authentication Tests\n'));
5
+
6
+ const moduleName = definition.moduleName || definition.getName?.() || 'unknown';
7
+ const results = {
8
+ testAuthRequest: { status: 'pending' },
9
+ getEntityDetails: { status: 'pending' },
10
+ getCredentialDetails: { status: 'pending' },
11
+ tokenRefresh: { status: 'pending' },
12
+ credentialProps: { set: 0, total: 0 },
13
+ entityProps: { set: 0, total: 0 },
14
+ };
15
+
16
+ // 1. Create fresh API instance with credentials
17
+ const apiParams = {
18
+ ...definition.env,
19
+ ...credentials.tokens,
20
+ ...credentials.apiParams,
21
+ };
22
+
23
+ const api = new ApiClass(apiParams);
24
+
25
+ // If API key, set it
26
+ if (credentials.apiKey) {
27
+ if (typeof api.setApiKey === 'function') {
28
+ api.setApiKey(credentials.apiKey);
29
+ } else {
30
+ api.api_key = credentials.apiKey;
31
+ api.access_token = credentials.apiKey;
32
+ }
33
+ }
34
+
35
+ // 2. Run testAuthRequest
36
+ console.log(chalk.gray('1. Running testAuthRequest...'));
37
+ try {
38
+ let testResult;
39
+ if (definition.requiredAuthMethods?.testAuthRequest) {
40
+ testResult = await definition.requiredAuthMethods.testAuthRequest(api);
41
+ } else {
42
+ testResult = await tryCommonTestMethods(api);
43
+ }
44
+
45
+ console.log(chalk.green(' โœ“ testAuthRequest passed'));
46
+ results.testAuthRequest = { status: 'passed' };
47
+
48
+ if (options.verbose && testResult) {
49
+ console.log(chalk.gray(' Response preview:'));
50
+ const preview = JSON.stringify(testResult, null, 2);
51
+ const truncated = preview.length > 500 ? preview.slice(0, 500) + '\n ...' : preview;
52
+ console.log(chalk.gray(' ' + truncated.split('\n').join('\n ')));
53
+ }
54
+ } catch (error) {
55
+ console.log(chalk.red(' โœ— testAuthRequest failed'));
56
+ console.log(chalk.red(` Error: ${error.message}`));
57
+ results.testAuthRequest = { status: 'failed', error: error.message };
58
+ if (options.verbose && error.stack) {
59
+ console.log(chalk.gray(` Stack: ${error.stack.split('\n').slice(1, 4).join('\n ')}`));
60
+ }
61
+ throw new Error(`Authentication test failed: ${error.message}`);
62
+ }
63
+
64
+ // 3. Test getEntityDetails
65
+ console.log(chalk.gray('\n2. Testing getEntityDetails...'));
66
+ const entityResult = await testGetEntityDetails(definition, api, credentials, options);
67
+ if (entityResult.skipped) {
68
+ console.log(chalk.yellow(` โš  Skipped (${entityResult.reason})`));
69
+ results.getEntityDetails = { status: 'skipped', reason: entityResult.reason };
70
+ } else if (entityResult.error) {
71
+ console.log(chalk.red(` โœ— Failed: ${entityResult.error}`));
72
+ results.getEntityDetails = { status: 'failed', error: entityResult.error };
73
+ } else if (!entityResult.consistent) {
74
+ console.log(chalk.yellow(` โš  Entity mismatch (saved: ${entityResult.savedId}, fresh: ${entityResult.freshId})`));
75
+ results.getEntityDetails = { status: 'warning', message: 'entity mismatch' };
76
+ } else {
77
+ console.log(chalk.green(` โœ“ getEntityDetails returned consistent entity${entityResult.freshId ? ` (externalId: ${entityResult.freshId})` : ''}`));
78
+ results.getEntityDetails = { status: 'passed' };
79
+ }
80
+
81
+ // 4. Test getCredentialDetails
82
+ console.log(chalk.gray('\n3. Testing getCredentialDetails...'));
83
+ const credResult = await testGetCredentialDetails(definition, api, options);
84
+ if (credResult.skipped) {
85
+ console.log(chalk.yellow(` โš  Skipped (${credResult.reason})`));
86
+ results.getCredentialDetails = { status: 'skipped', reason: credResult.reason };
87
+ } else if (credResult.error) {
88
+ console.log(chalk.red(` โœ— Failed: ${credResult.error}`));
89
+ results.getCredentialDetails = { status: 'failed', error: credResult.error };
90
+ } else if (!credResult.valid) {
91
+ console.log(chalk.yellow(' โš  getCredentialDetails did not return valid identifiers'));
92
+ results.getCredentialDetails = { status: 'warning', message: 'invalid structure' };
93
+ } else {
94
+ console.log(chalk.green(' โœ“ getCredentialDetails returned valid identifiers'));
95
+ results.getCredentialDetails = { status: 'passed' };
96
+ if (options.verbose && credResult.credentials?.identifiers) {
97
+ const ids = credResult.credentials.identifiers;
98
+ console.log(chalk.gray(` Identifiers: ${JSON.stringify(ids)}`));
99
+ }
100
+ }
101
+
102
+ // 5. Test token refresh
103
+ console.log(chalk.gray('\n4. Testing token refresh...'));
104
+ const refreshResult = await testTokenRefresh(api, credentials, options);
105
+ if (refreshResult.skipped) {
106
+ console.log(chalk.yellow(` โš  Skipped (${refreshResult.reason})`));
107
+ results.tokenRefresh = { status: 'skipped', reason: refreshResult.reason };
108
+ } else if (refreshResult.error) {
109
+ console.log(chalk.red(` โœ— Failed: ${refreshResult.error}`));
110
+ results.tokenRefresh = { status: 'failed', error: refreshResult.error };
111
+ } else {
112
+ const tokenMsg = refreshResult.tokenChanged ? 'token refreshed successfully' : 'refresh called but token unchanged';
113
+ console.log(chalk.green(` โœ“ ${tokenMsg}`));
114
+ results.tokenRefresh = { status: 'passed', tokenChanged: refreshResult.tokenChanged };
115
+ }
116
+
117
+ // 6. Verify credential persistence properties
118
+ console.log(chalk.gray('\n5. Verifying credential properties (apiPropertiesToPersist.credential)...'));
119
+ const credProps = definition.requiredAuthMethods?.apiPropertiesToPersist?.credential || [];
120
+ results.credentialProps.total = credProps.length;
121
+
122
+ if (credProps.length === 0) {
123
+ console.log(chalk.gray(' (no credential properties defined)'));
124
+ } else {
125
+ for (const prop of credProps) {
126
+ const value = api[prop];
127
+ if (value !== undefined && value !== null && value !== '') {
128
+ console.log(chalk.green(` โœ“ ${prop}: ${maskSensitive(prop, value)}`));
129
+ results.credentialProps.set++;
130
+ } else {
131
+ console.log(chalk.yellow(` โš  ${prop}: not set or empty`));
132
+ }
133
+ }
134
+ }
135
+
136
+ // 7. Verify entity persistence properties
137
+ console.log(chalk.gray('\n6. Verifying entity properties (apiPropertiesToPersist.entity)...'));
138
+ const entityProps = definition.requiredAuthMethods?.apiPropertiesToPersist?.entity || [];
139
+ results.entityProps.total = entityProps.length;
140
+
141
+ if (entityProps.length === 0) {
142
+ console.log(chalk.gray(' (no entity properties defined)'));
143
+ } else {
144
+ for (const prop of entityProps) {
145
+ const value = api[prop];
146
+ if (value !== undefined && value !== null && value !== '') {
147
+ console.log(chalk.green(` โœ“ ${prop}: ${maskSensitive(prop, value)}`));
148
+ results.entityProps.set++;
149
+ } else {
150
+ console.log(chalk.yellow(` โš  ${prop}: not set or empty`));
151
+ }
152
+ }
153
+ }
154
+
155
+ // 8. Summary
156
+ console.log(chalk.blue('\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'));
157
+ console.log(chalk.blue('Summary'));
158
+ console.log(chalk.blue('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n'));
159
+
160
+ printSummaryLine('testAuthRequest', results.testAuthRequest);
161
+ printSummaryLine('getEntityDetails', results.getEntityDetails);
162
+ printSummaryLine('getCredentialDetails', results.getCredentialDetails);
163
+ printSummaryLine('tokenRefresh', results.tokenRefresh);
164
+
165
+ const credPropsStatus = results.credentialProps.total === 0
166
+ ? chalk.gray('n/a')
167
+ : (results.credentialProps.set === results.credentialProps.total
168
+ ? chalk.green(`${results.credentialProps.set}/${results.credentialProps.total} set`)
169
+ : chalk.yellow(`${results.credentialProps.set}/${results.credentialProps.total} set`));
170
+ console.log(` credentialProps: ${credPropsStatus}`);
171
+
172
+ const entityPropsStatus = results.entityProps.total === 0
173
+ ? chalk.gray('n/a')
174
+ : (results.entityProps.set === results.entityProps.total
175
+ ? chalk.green(`${results.entityProps.set}/${results.entityProps.total} set`)
176
+ : chalk.yellow(`${results.entityProps.set}/${results.entityProps.total} set`));
177
+ console.log(` entityProps: ${entityPropsStatus}`);
178
+
179
+ console.log('');
180
+
181
+ // Check if any critical tests failed
182
+ const criticalFailed = results.testAuthRequest.status === 'failed';
183
+ if (criticalFailed) {
184
+ throw new Error('Critical authentication tests failed');
185
+ }
186
+
187
+ console.log(chalk.green('โœ“ All authentication tests passed'));
188
+
189
+ return {
190
+ testAuthRequestPassed: results.testAuthRequest.status === 'passed',
191
+ getEntityDetailsPassed: results.getEntityDetails.status === 'passed',
192
+ getCredentialDetailsPassed: results.getCredentialDetails.status === 'passed',
193
+ tokenRefreshPassed: results.tokenRefresh.status === 'passed',
194
+ credentialPropertiesValid: results.credentialProps.set === results.credentialProps.total,
195
+ entityPropertiesValid: results.entityProps.set === results.entityProps.total,
196
+ };
197
+ }
198
+
199
+ function printSummaryLine(name, result) {
200
+ const paddedName = (name + ':').padEnd(22);
201
+ let statusText;
202
+
203
+ switch (result.status) {
204
+ case 'passed':
205
+ statusText = chalk.green('โœ“ passed');
206
+ break;
207
+ case 'failed':
208
+ statusText = chalk.red('โœ— failed');
209
+ break;
210
+ case 'skipped':
211
+ statusText = chalk.yellow(`โš  skipped${result.reason ? ` (${result.reason})` : ''}`);
212
+ break;
213
+ case 'warning':
214
+ statusText = chalk.yellow(`โš  ${result.message || 'warning'}`);
215
+ break;
216
+ default:
217
+ statusText = chalk.gray('pending');
218
+ }
219
+
220
+ console.log(` ${paddedName}${statusText}`);
221
+ }
222
+
223
+ async function testGetEntityDetails(definition, api, savedCredentials, options) {
224
+ if (!definition.requiredAuthMethods?.getEntityDetails) {
225
+ return { skipped: true, reason: 'not defined' };
226
+ }
227
+
228
+ try {
229
+ const freshEntity = await definition.requiredAuthMethods.getEntityDetails(
230
+ api,
231
+ {}, // callbackParams
232
+ {}, // tokenResponse
233
+ 'cli-test-user'
234
+ );
235
+
236
+ const savedId = savedCredentials.entity?.identifiers?.externalId;
237
+ const freshId = freshEntity?.identifiers?.externalId;
238
+ const consistent = !savedId || !freshId || savedId === freshId;
239
+
240
+ return {
241
+ success: true,
242
+ consistent,
243
+ freshId,
244
+ savedId,
245
+ freshEntity
246
+ };
247
+ } catch (error) {
248
+ return { error: error.message };
249
+ }
250
+ }
251
+
252
+ async function testGetCredentialDetails(definition, api, options) {
253
+ if (!definition.requiredAuthMethods?.getCredentialDetails) {
254
+ return { skipped: true, reason: 'not defined' };
255
+ }
256
+
257
+ try {
258
+ const credentials = await definition.requiredAuthMethods.getCredentialDetails(
259
+ api,
260
+ 'cli-test-user'
261
+ );
262
+
263
+ const valid = credentials && typeof credentials.identifiers === 'object';
264
+ return { success: true, valid, credentials };
265
+ } catch (error) {
266
+ return { error: error.message };
267
+ }
268
+ }
269
+
270
+ async function testTokenRefresh(api, savedCredentials, options) {
271
+ // Check if refresh token exists
272
+ if (!savedCredentials.tokens?.refresh_token) {
273
+ return { skipped: true, reason: 'no refresh token' };
274
+ }
275
+
276
+ // Check if API supports refresh
277
+ if (typeof api.refreshAccessToken !== 'function') {
278
+ return { skipped: true, reason: 'refreshAccessToken not implemented' };
279
+ }
280
+
281
+ try {
282
+ const oldToken = api.access_token;
283
+ await api.refreshAccessToken();
284
+ const newToken = api.access_token;
285
+
286
+ return {
287
+ success: true,
288
+ tokenChanged: newToken !== oldToken
289
+ };
290
+ } catch (error) {
291
+ return { error: error.message };
292
+ }
293
+ }
294
+
295
+ async function tryCommonTestMethods(api) {
296
+ const methodsToTry = [
297
+ 'getUserDetails',
298
+ 'getUser',
299
+ 'getCurrentUser',
300
+ 'getMe',
301
+ 'getAccount',
302
+ 'getProfile',
303
+ ];
304
+
305
+ for (const method of methodsToTry) {
306
+ if (typeof api[method] === 'function') {
307
+ return await api[method]();
308
+ }
309
+ }
310
+
311
+ throw new Error('No testAuthRequest method defined and no common test methods available');
312
+ }
313
+
314
+ function maskSensitive(prop, value) {
315
+ const sensitiveProps = [
316
+ 'access_token',
317
+ 'refresh_token',
318
+ 'api_key',
319
+ 'apiKey',
320
+ 'client_secret',
321
+ 'password',
322
+ 'secret',
323
+ 'token',
324
+ ];
325
+
326
+ const propLower = prop.toLowerCase();
327
+ const isSensitive = sensitiveProps.some(sp => propLower.includes(sp.toLowerCase()));
328
+
329
+ if (isSensitive && typeof value === 'string') {
330
+ if (value.length <= 8) {
331
+ return '***';
332
+ }
333
+ return value.slice(0, 4) + '...' + value.slice(-4);
334
+ }
335
+
336
+ const strValue = String(value);
337
+ if (strValue.length > 50) {
338
+ return strValue.slice(0, 47) + '...';
339
+ }
340
+
341
+ return strValue;
342
+ }
343
+
344
+ module.exports = { runAuthTests };
@@ -0,0 +1,182 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ class CredentialStorage {
6
+ constructor(options = {}) {
7
+ // Global storage in user home directory
8
+ this.globalPath = path.join(os.homedir(), '.frigg-credentials.json');
9
+ // Project-local storage (if in a Frigg project)
10
+ this.localPath = path.join(process.cwd(), '.frigg-credentials.json');
11
+ // Allow override via options
12
+ this.customPath = options.path || null;
13
+ }
14
+
15
+ getStoragePath() {
16
+ // Priority: custom > local (if exists) > global
17
+ if (this.customPath) {
18
+ return this.customPath;
19
+ }
20
+ if (fs.existsSync(this.localPath)) {
21
+ return this.localPath;
22
+ }
23
+ return this.globalPath;
24
+ }
25
+
26
+ getWritePath() {
27
+ // Priority: custom > local (if in project) > global
28
+ if (this.customPath) {
29
+ return this.customPath;
30
+ }
31
+ if (this.isInProject()) {
32
+ return this.localPath;
33
+ }
34
+ return this.globalPath;
35
+ }
36
+
37
+ async load() {
38
+ const filePath = this.getStoragePath();
39
+
40
+ if (!fs.existsSync(filePath)) {
41
+ return {
42
+ _meta: {
43
+ version: 1,
44
+ warning: 'DO NOT COMMIT THIS FILE - contains sensitive credentials'
45
+ },
46
+ modules: {}
47
+ };
48
+ }
49
+
50
+ try {
51
+ const content = fs.readFileSync(filePath, 'utf8');
52
+ return JSON.parse(content);
53
+ } catch (err) {
54
+ console.warn(`Warning: Could not read credentials file: ${err.message}`);
55
+ return { _meta: { version: 1 }, modules: {} };
56
+ }
57
+ }
58
+
59
+ async save(moduleName, credentials, authType) {
60
+ const data = await this.load();
61
+
62
+ data.modules[moduleName] = {
63
+ ...credentials,
64
+ authType,
65
+ savedAt: new Date().toISOString(),
66
+ };
67
+
68
+ const targetPath = this.getWritePath();
69
+
70
+ // Ensure directory exists
71
+ const dir = path.dirname(targetPath);
72
+ if (!fs.existsSync(dir)) {
73
+ fs.mkdirSync(dir, { recursive: true });
74
+ }
75
+
76
+ fs.writeFileSync(targetPath, JSON.stringify(data, null, 2));
77
+
78
+ // Add to .gitignore if saving locally
79
+ if (targetPath === this.localPath) {
80
+ this.ensureGitIgnore();
81
+ }
82
+
83
+ return targetPath;
84
+ }
85
+
86
+ async get(moduleName) {
87
+ const data = await this.load();
88
+ return data.modules[moduleName] || null;
89
+ }
90
+
91
+ async list() {
92
+ const data = await this.load();
93
+ return Object.entries(data.modules).map(([name, creds]) => ({
94
+ module: name,
95
+ authType: creds.authType,
96
+ savedAt: creds.savedAt,
97
+ entity: creds.entity?.details?.name || creds.entity?.identifiers?.externalId || 'Unknown',
98
+ hasAccessToken: !!(creds.tokens?.access_token || creds.apiKey),
99
+ hasRefreshToken: !!creds.tokens?.refresh_token,
100
+ }));
101
+ }
102
+
103
+ async delete(moduleName) {
104
+ const data = await this.load();
105
+
106
+ if (!data.modules[moduleName]) {
107
+ return false;
108
+ }
109
+
110
+ delete data.modules[moduleName];
111
+
112
+ const targetPath = this.getStoragePath();
113
+ fs.writeFileSync(targetPath, JSON.stringify(data, null, 2));
114
+ return true;
115
+ }
116
+
117
+ async deleteAll() {
118
+ const data = {
119
+ _meta: {
120
+ version: 1,
121
+ warning: 'DO NOT COMMIT THIS FILE - contains sensitive credentials'
122
+ },
123
+ modules: {}
124
+ };
125
+
126
+ const targetPath = this.getStoragePath();
127
+ fs.writeFileSync(targetPath, JSON.stringify(data, null, 2));
128
+ }
129
+
130
+ isInProject() {
131
+ // Check for indicators of a Frigg project
132
+ const indicators = [
133
+ path.join(process.cwd(), 'backend', 'index.js'),
134
+ path.join(process.cwd(), 'infrastructure.js'),
135
+ path.join(process.cwd(), 'backend', 'infrastructure.js'),
136
+ path.join(process.cwd(), 'package.json'),
137
+ ];
138
+
139
+ for (const indicator of indicators) {
140
+ if (fs.existsSync(indicator)) {
141
+ // Additional check: look for frigg-related content in package.json
142
+ if (indicator.endsWith('package.json')) {
143
+ try {
144
+ const pkg = JSON.parse(fs.readFileSync(indicator, 'utf8'));
145
+ if (pkg.dependencies?.['@friggframework/core'] ||
146
+ pkg.devDependencies?.['@friggframework/core'] ||
147
+ pkg.name?.includes('frigg')) {
148
+ return true;
149
+ }
150
+ } catch {
151
+ // Ignore parse errors
152
+ }
153
+ } else {
154
+ return true;
155
+ }
156
+ }
157
+ }
158
+
159
+ return false;
160
+ }
161
+
162
+ ensureGitIgnore() {
163
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
164
+ const entry = '.frigg-credentials.json';
165
+
166
+ try {
167
+ if (fs.existsSync(gitignorePath)) {
168
+ const content = fs.readFileSync(gitignorePath, 'utf8');
169
+ if (!content.includes(entry)) {
170
+ fs.appendFileSync(gitignorePath, `\n# Frigg auth credentials (DO NOT COMMIT)\n${entry}\n`);
171
+ }
172
+ } else {
173
+ // Create new .gitignore
174
+ fs.writeFileSync(gitignorePath, `# Frigg auth credentials (DO NOT COMMIT)\n${entry}\n`);
175
+ }
176
+ } catch (err) {
177
+ console.warn(`Warning: Could not update .gitignore: ${err.message}`);
178
+ }
179
+ }
180
+ }
181
+
182
+ module.exports = { CredentialStorage };