@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,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 };
|