@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.
@@ -0,0 +1,450 @@
1
+ # Frigg Authenticator
2
+
3
+ A CLI tool for testing OAuth2 and API-Key authentication flows in Frigg API modules without deploying full infrastructure.
4
+
5
+ ## Overview
6
+
7
+ The Frigg Authenticator allows API module developers to:
8
+ - Test OAuth2 authentication flows end-to-end
9
+ - Test API-Key authentication
10
+ - Verify `requiredAuthMethods` work correctly
11
+ - Save credentials for reuse in tests
12
+ - Debug authentication issues quickly
13
+
14
+ ## Installation
15
+
16
+ The authenticator is included in `@friggframework/devtools` and available via the Frigg CLI:
17
+
18
+ ```bash
19
+ npm install @friggframework/devtools
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ### Testing OAuth2 Modules
25
+
26
+ ```bash
27
+ # Navigate to your API module directory
28
+ cd packages/api-module-attio
29
+
30
+ # Ensure .env has required OAuth credentials
31
+ cat .env
32
+ # ATTIO_CLIENT_ID=your_client_id
33
+ # ATTIO_CLIENT_SECRET=your_client_secret
34
+ # ATTIO_SCOPE=read:objects write:objects
35
+ # REDIRECT_URI=http://localhost:3333
36
+
37
+ # Run the auth test
38
+ frigg auth test .
39
+ ```
40
+
41
+ This will:
42
+ 1. Load your module and validate its definition
43
+ 2. Start a local callback server on port 3333
44
+ 3. Open your browser to the OAuth authorization page
45
+ 4. Capture the callback and exchange code for tokens
46
+ 5. Run `testAuthRequest` to verify authentication works
47
+ 6. Save credentials to `.frigg-credentials.json`
48
+
49
+ ### Testing API-Key Modules
50
+
51
+ API-Key modules with `getAuthorizationRequirements` will render an interactive JSON Schema form:
52
+
53
+ ```bash
54
+ # Navigate to API module directory
55
+ cd packages/api-module-quo
56
+
57
+ # Run auth test - renders interactive form
58
+ frigg auth test .
59
+ ```
60
+
61
+ **Interactive Form Example:**
62
+ ```
63
+ šŸ“ Quo API Authorization
64
+
65
+ (Your Quo API key)
66
+ API Key: ********************************
67
+
68
+ šŸ”‘ API-Key Authentication Flow
69
+
70
+ Module: quo
71
+ āœ“ API key configured
72
+ Fetching entity details...
73
+ āœ“ Entity details retrieved
74
+ Entity: Quo Workspace (API Key Hash)
75
+ ```
76
+
77
+ The form:
78
+ - Displays title from `jsonSchema.title`
79
+ - Shows help text from `ui:help` before each field
80
+ - Masks password fields (`ui:widget: 'password'`) with `*`
81
+ - Validates required fields
82
+
83
+ **Using `--api-key` flag (bypasses interactive form):**
84
+ ```bash
85
+ frigg auth test ./my-api-key-module --api-key YOUR_API_KEY
86
+ ```
87
+
88
+ ## Commands
89
+
90
+ ### `frigg auth test <module>`
91
+
92
+ Test authentication for an API module.
93
+
94
+ **Arguments:**
95
+ - `<module>` - Module path or name. Can be:
96
+ - `.` - Current directory
97
+ - `./path/to/module` - Relative path
98
+ - `/absolute/path/to/module` - Absolute path
99
+ - `attio` - Short name (resolves to `@friggframework/api-module-attio`)
100
+ - `@friggframework/api-module-attio` - Full package name
101
+
102
+ **Options:**
103
+ | Option | Default | Description |
104
+ |--------|---------|-------------|
105
+ | `--api-key <key>` | - | API key (bypasses interactive form) |
106
+ | `--port <port>` | `3333` | Callback server port |
107
+ | `--no-browser` | `false` | Don't auto-open browser (print URL instead) |
108
+ | `--timeout <seconds>` | `300` | OAuth callback timeout |
109
+ | `-v, --verbose` | `false` | Enable verbose output |
110
+
111
+ **Examples:**
112
+ ```bash
113
+ # Test current directory module
114
+ frigg auth test .
115
+
116
+ # Test with custom port
117
+ frigg auth test . --port 8080
118
+
119
+ # Test without opening browser
120
+ frigg auth test . --no-browser
121
+
122
+ # Test API-Key module
123
+ frigg auth test . --api-key sk_live_xxxxx
124
+
125
+ # Verbose output for debugging
126
+ frigg auth test . --verbose
127
+ ```
128
+
129
+ ### `frigg auth list`
130
+
131
+ List all saved credentials.
132
+
133
+ **Options:**
134
+ | Option | Description |
135
+ |--------|-------------|
136
+ | `--json` | Output as JSON |
137
+
138
+ **Example:**
139
+ ```bash
140
+ frigg auth list
141
+ ```
142
+
143
+ Output:
144
+ ```
145
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
146
+ │ Module │ Auth Type │ Entity │ Has Access Token │ Has Refresh Token │ Saved At │
147
+ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
148
+ │ attio │ oauth2 │ Left Hook Dev │ āœ“ │ - │ 12/23/2025, 7:34 PM │
149
+ │ quo │ apiKey │ Quo Workspace │ āœ“ │ - │ 12/23/2025, 8:00 PM │
150
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
151
+ ```
152
+
153
+ ### `frigg auth get <module>`
154
+
155
+ Get credentials for a specific module.
156
+
157
+ **Options:**
158
+ | Option | Description |
159
+ |--------|-------------|
160
+ | `--json` | Output as JSON (for scripts) |
161
+ | `--export` | Output as environment variables |
162
+
163
+ **Examples:**
164
+ ```bash
165
+ # Display formatted credentials
166
+ frigg auth get attio
167
+
168
+ # Get as JSON for scripts
169
+ frigg auth get attio --json
170
+
171
+ # Export as environment variables
172
+ eval $(frigg auth get attio --export)
173
+ ```
174
+
175
+ ### `frigg auth delete [module]`
176
+
177
+ Delete saved credentials.
178
+
179
+ **Options:**
180
+ | Option | Description |
181
+ |--------|-------------|
182
+ | `--all` | Delete all credentials |
183
+ | `-y, --yes` | Skip confirmation |
184
+
185
+ **Examples:**
186
+ ```bash
187
+ # Delete specific module credentials
188
+ frigg auth delete attio
189
+
190
+ # Delete all credentials
191
+ frigg auth delete --all
192
+
193
+ # Skip confirmation
194
+ frigg auth delete attio -y
195
+ ```
196
+
197
+ ## Credential Storage
198
+
199
+ Credentials are saved to `.frigg-credentials.json`:
200
+ - **Project-local**: If run in a Frigg project, saves to project root
201
+ - **Global**: Otherwise saves to `~/.frigg-credentials.json`
202
+
203
+ The file is automatically added to `.gitignore` when saving locally.
204
+
205
+ ### Credential Format
206
+
207
+ ```json
208
+ {
209
+ "_meta": {
210
+ "version": 1,
211
+ "warning": "DO NOT COMMIT THIS FILE - contains sensitive credentials"
212
+ },
213
+ "modules": {
214
+ "attio": {
215
+ "authType": "oauth2",
216
+ "tokens": {
217
+ "access_token": "xxx...",
218
+ "refresh_token": null,
219
+ "accessTokenExpire": "2025-12-24T19:34:50.468Z"
220
+ },
221
+ "entity": {
222
+ "identifiers": {
223
+ "externalId": "workspace-id",
224
+ "user": "cli-test-user"
225
+ },
226
+ "details": {
227
+ "name": "My Workspace"
228
+ }
229
+ },
230
+ "obtainedAt": "2025-12-23T19:34:51.097Z",
231
+ "savedAt": "2025-12-23T19:34:51.713Z"
232
+ }
233
+ }
234
+ }
235
+ ```
236
+
237
+ ## Using Credentials in Tests
238
+
239
+ ### Direct Import
240
+
241
+ ```javascript
242
+ const { CredentialStorage } = require('@friggframework/devtools/frigg-cli/auth-command/credential-storage');
243
+
244
+ describe('Attio Integration', () => {
245
+ let api;
246
+
247
+ beforeAll(async () => {
248
+ const storage = new CredentialStorage();
249
+ const credentials = await storage.get('attio');
250
+
251
+ if (!credentials) {
252
+ throw new Error('Run "frigg auth test attio" first');
253
+ }
254
+
255
+ const { Api } = require('@friggframework/api-module-attio');
256
+ api = new Api({
257
+ ...credentials.tokens,
258
+ ...credentials.apiParams,
259
+ });
260
+ });
261
+
262
+ it('should list objects', async () => {
263
+ const result = await api.listObjects();
264
+ expect(result.data).toBeDefined();
265
+ });
266
+ });
267
+ ```
268
+
269
+ ### Environment Variables
270
+
271
+ ```bash
272
+ # Export credentials as env vars
273
+ eval $(frigg auth get attio --export)
274
+
275
+ # Now available as:
276
+ # ATTIO_ACCESS_TOKEN=xxx
277
+ # ATTIO_EXTERNAL_ID=workspace-id
278
+ ```
279
+
280
+ ## Module Requirements
281
+
282
+ For the authenticator to work, your module must have:
283
+
284
+ ### Required Definition Fields
285
+
286
+ ```javascript
287
+ const Definition = {
288
+ API: Api, // API class (extends OAuth2Requester or ApiKeyRequester)
289
+ moduleName: 'my-module', // Unique module name
290
+ requiredAuthMethods: {
291
+ getToken, // Exchange auth code for tokens (OAuth2)
292
+ getEntityDetails, // Get entity info after auth
293
+ getCredentialDetails, // Get credential info
294
+ testAuthRequest, // Verify auth works
295
+ apiPropertiesToPersist, // Fields to persist
296
+ },
297
+ env: {
298
+ client_id: process.env.MY_MODULE_CLIENT_ID,
299
+ client_secret: process.env.MY_MODULE_CLIENT_SECRET,
300
+ scope: process.env.MY_MODULE_SCOPE,
301
+ redirect_uri: process.env.REDIRECT_URI,
302
+ }
303
+ };
304
+ ```
305
+
306
+ ### Required Environment Variables
307
+
308
+ Create a `.env` file in your module directory:
309
+
310
+ ```bash
311
+ # OAuth2 modules
312
+ MY_MODULE_CLIENT_ID=your_client_id
313
+ MY_MODULE_CLIENT_SECRET=your_client_secret
314
+ MY_MODULE_SCOPE=read write
315
+ REDIRECT_URI=http://localhost:3333
316
+
317
+ # API-Key modules - use interactive form or --api-key flag
318
+ ```
319
+
320
+ ### JSON Schema Form for API-Key Modules
321
+
322
+ API-Key modules can define `getAuthorizationRequirements` to enable interactive CLI forms:
323
+
324
+ ```javascript
325
+ // definition.js
326
+ const Definition = {
327
+ // ...
328
+ requiredAuthMethods: {
329
+ getAuthorizationRequirements: (api) => ({
330
+ type: 'apiKey',
331
+ data: {
332
+ jsonSchema: {
333
+ title: 'My API Authorization',
334
+ type: 'object',
335
+ required: ['apiKey'],
336
+ properties: {
337
+ apiKey: { type: 'string', title: 'API Key' }
338
+ }
339
+ },
340
+ uiSchema: {
341
+ apiKey: {
342
+ 'ui:widget': 'password', // Masks input with *
343
+ 'ui:help': 'Your API key from the dashboard'
344
+ }
345
+ }
346
+ }
347
+ }),
348
+ // ... other methods
349
+ }
350
+ };
351
+ ```
352
+
353
+ **Multi-Field Example (e.g., ConnectWise):**
354
+ ```javascript
355
+ getAuthorizationRequirements: (api) => ({
356
+ type: 'apiKey',
357
+ data: {
358
+ jsonSchema: {
359
+ title: 'ConnectWise Authentication',
360
+ type: 'object',
361
+ required: ['companyId', 'publicKey', 'privateKey'],
362
+ properties: {
363
+ companyId: { type: 'string', title: 'Company ID' },
364
+ publicKey: { type: 'string', title: 'Public Key' },
365
+ privateKey: { type: 'string', title: 'Private Key' },
366
+ siteUrl: { type: 'string', title: 'Site URL' }
367
+ }
368
+ },
369
+ uiSchema: {
370
+ companyId: { 'ui:help': 'The Company ID you use to login' },
371
+ publicKey: { 'ui:help': 'From My Account > API Keys' },
372
+ privateKey: { 'ui:widget': 'password', 'ui:help': 'Your private key' },
373
+ siteUrl: { 'ui:help': 'e.g., https://na.myconnectwise.net' }
374
+ }
375
+ }
376
+ })
377
+ ```
378
+
379
+ **Supported UI Schema Options:**
380
+ - `ui:widget: 'password'` - Masks input with `*` characters
381
+ - `ui:help` - Displays help text before the field prompt
382
+
383
+ ## Troubleshooting
384
+
385
+ ### Port Already in Use
386
+
387
+ ```
388
+ Error: Port 3333 is already in use.
389
+ Try using a different port: frigg auth test <module> --port <different-port>
390
+ ```
391
+
392
+ **Solution:** Use a different port with `--port 8080`
393
+
394
+ ### Module Not Found
395
+
396
+ ```
397
+ Error: Could not find module: my-module
398
+ ```
399
+
400
+ **Solution:**
401
+ - Use `.` for current directory
402
+ - Use absolute path
403
+ - Ensure module exports `Definition`
404
+
405
+ ### OAuth Callback Timeout
406
+
407
+ ```
408
+ Error: OAuth callback timeout after 300 seconds.
409
+ ```
410
+
411
+ **Solution:**
412
+ - Complete authorization in browser faster
413
+ - Increase timeout with `--timeout 600`
414
+ - Check redirect URI matches callback server
415
+
416
+ ### Invalid Module Definition
417
+
418
+ ```
419
+ Error: Module validation failed:
420
+ - Missing required auth method: testAuthRequest
421
+ ```
422
+
423
+ **Solution:** Ensure your module's `requiredAuthMethods` has all required methods.
424
+
425
+ ## Architecture
426
+
427
+ ```
428
+ auth-command/
429
+ ā”œā”€ā”€ index.js # Main command handler
430
+ ā”œā”€ā”€ module-loader.js # Dynamic module loading & validation
431
+ ā”œā”€ā”€ credential-storage.js # .frigg-credentials.json persistence
432
+ ā”œā”€ā”€ oauth-callback-server.js # Local HTTP server for OAuth callbacks
433
+ ā”œā”€ā”€ oauth-flow.js # OAuth2 flow orchestration
434
+ ā”œā”€ā”€ api-key-flow.js # API-Key authentication flow
435
+ ā”œā”€ā”€ json-schema-form.js # Interactive JSON Schema form renderer
436
+ ā”œā”€ā”€ auth-tester.js # Run testAuthRequest & sample API calls
437
+ └── utils/
438
+ └── browser.js # Cross-platform browser opening
439
+ ```
440
+
441
+ ## Security Considerations
442
+
443
+ 1. **Credentials are stored in plain text** - Only use for development/testing
444
+ 2. **Auto-adds to .gitignore** - Prevents accidental commits
445
+ 3. **CSRF protection** - OAuth state parameter prevents attacks
446
+ 4. **Local callback only** - Server only listens on localhost
447
+
448
+ ## Contributing
449
+
450
+ See the main [Frigg Contributing Guide](https://github.com/friggframework/frigg/blob/main/CONTRIBUTING.md).
@@ -0,0 +1,153 @@
1
+ const chalk = require('chalk');
2
+ const { renderJsonSchemaForm } = require('./json-schema-form');
3
+
4
+ async function runApiKeyFlow(definition, ApiClass, providedApiKey, options) {
5
+ const moduleName = definition.moduleName || definition.getName?.() || 'unknown';
6
+
7
+ let apiKey = providedApiKey;
8
+ let formData = null;
9
+
10
+ // If no API key provided, check for getAuthorizationRequirements to render form
11
+ if (!apiKey) {
12
+ if (definition.requiredAuthMethods?.getAuthorizationRequirements) {
13
+ // Create temporary API instance to call getAuthorizationRequirements
14
+ const tempApi = new ApiClass({ ...definition.env });
15
+ const authReqs = definition.requiredAuthMethods.getAuthorizationRequirements(tempApi);
16
+
17
+ if (authReqs?.data?.jsonSchema) {
18
+ // Render the JSON schema form
19
+ formData = await renderJsonSchemaForm(
20
+ authReqs.data.jsonSchema,
21
+ authReqs.data.uiSchema
22
+ );
23
+
24
+ // Extract API key from form data - try common field names
25
+ apiKey = formData.apiKey || formData.api_key ||
26
+ formData.access_token || formData.token;
27
+
28
+ // If still no API key found, use the first value from the form
29
+ if (!apiKey && Object.keys(formData).length > 0) {
30
+ apiKey = Object.values(formData)[0];
31
+ }
32
+
33
+ if (!apiKey) {
34
+ throw new Error('No API key provided in form');
35
+ }
36
+ }
37
+ }
38
+
39
+ if (!apiKey) {
40
+ throw new Error(
41
+ `--api-key is required for API-Key modules without getAuthorizationRequirements.\n` +
42
+ `Usage: frigg auth test ${moduleName} --api-key YOUR_API_KEY`
43
+ );
44
+ }
45
+ }
46
+
47
+ console.log(chalk.blue('\nšŸ”‘ API-Key Authentication Flow\n'));
48
+ console.log(chalk.gray(`Module: ${moduleName}`));
49
+
50
+ // 1. Create API instance with environment params
51
+ const apiParams = {
52
+ ...definition.env,
53
+ };
54
+ const api = new ApiClass(apiParams);
55
+
56
+ // 2. Set API key using available methods
57
+ let apiKeySet = false;
58
+
59
+ // Try different methods to set the API key
60
+ if (definition.requiredAuthMethods?.setAuthParams) {
61
+ // Some modules have setAuthParams in definition
62
+ await definition.requiredAuthMethods.setAuthParams(api, {
63
+ apiKey,
64
+ data: { apiKey, api_key: apiKey, access_token: apiKey }
65
+ });
66
+ apiKeySet = true;
67
+ } else if (typeof api.setApiKey === 'function') {
68
+ // Standard ApiKeyRequester method
69
+ api.setApiKey(apiKey);
70
+ apiKeySet = true;
71
+ } else if (typeof api.setAuthParams === 'function') {
72
+ // Alternative method name
73
+ await api.setAuthParams({ apiKey, api_key: apiKey, access_token: apiKey });
74
+ apiKeySet = true;
75
+ } else {
76
+ // Direct property assignment as fallback
77
+ api.api_key = apiKey;
78
+ api.access_token = apiKey;
79
+ apiKeySet = true;
80
+ }
81
+
82
+ if (!apiKeySet) {
83
+ throw new Error(
84
+ `Could not set API key for module ${moduleName}.\n` +
85
+ `Module does not have setApiKey(), setAuthParams(), or setAuthParams in requiredAuthMethods.`
86
+ );
87
+ }
88
+
89
+ console.log(chalk.green('āœ“ API key configured'));
90
+
91
+ // 3. Get entity details
92
+ console.log(chalk.gray('Fetching entity details...'));
93
+
94
+ let entityDetails;
95
+ if (definition.requiredAuthMethods?.getEntityDetails) {
96
+ try {
97
+ entityDetails = await definition.requiredAuthMethods.getEntityDetails(
98
+ api,
99
+ {},
100
+ { api_key: apiKey },
101
+ 'cli-test-user'
102
+ );
103
+ } catch (err) {
104
+ console.log(chalk.yellow(` Warning: getEntityDetails failed: ${err.message}`));
105
+ entityDetails = {
106
+ identifiers: { externalId: 'unknown', user: 'cli-test-user' },
107
+ details: { name: 'API Key Authentication' }
108
+ };
109
+ }
110
+ } else {
111
+ entityDetails = {
112
+ identifiers: { externalId: 'unknown', user: 'cli-test-user' },
113
+ details: { name: 'API Key Authentication' }
114
+ };
115
+ }
116
+
117
+ console.log(chalk.green('āœ“ Entity details retrieved'));
118
+
119
+ if (entityDetails?.details?.name) {
120
+ console.log(chalk.gray(` Entity: ${entityDetails.details.name}`));
121
+ }
122
+
123
+ // 4. Get credential details
124
+ let credentialDetails = {};
125
+ if (definition.requiredAuthMethods?.getCredentialDetails) {
126
+ try {
127
+ credentialDetails = await definition.requiredAuthMethods.getCredentialDetails(
128
+ api,
129
+ 'cli-test-user'
130
+ );
131
+ } catch (err) {
132
+ console.log(chalk.yellow(` Warning: Could not get credential details: ${err.message}`));
133
+ }
134
+ }
135
+
136
+ // 5. Return credentials object
137
+ return {
138
+ apiKey,
139
+ entity: entityDetails,
140
+ credential: credentialDetails,
141
+ apiParams: sanitizeApiParams(apiParams),
142
+ obtainedAt: new Date().toISOString(),
143
+ };
144
+ }
145
+
146
+ function sanitizeApiParams(params) {
147
+ // Remove sensitive data that shouldn't be stored in readable form
148
+ const sanitized = { ...params };
149
+ delete sanitized.client_secret;
150
+ return sanitized;
151
+ }
152
+
153
+ module.exports = { runApiKeyFlow };