{ "id": "3l7tJfcRoA1T1o6g", "name": "NPM Auth Token Manager", "nodes": [ { "parameters": {}, "id": "start-node", "name": "Manual Trigger", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 240, 320 ] }, { "parameters": { "rule": { "interval": [ { "field": "months", "monthsInterval": 11 } ] } }, "id": "schedule-node", "name": "Schedule Token Refresh", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, "position": [ 240, 480 ], "notes": "Runs every 11 months to check token validity and refresh if needed" }, { "parameters": { "jsCode": "// Check if existing token is still valid based on expiration date\nconst items_data = items || [];\n\nconsole.log('=== Token Validation Starting ===');\nconsole.log('Items received:', items_data.length);\n\n// If no token exists or Supabase query returned no data, proceed to generate new one\nif (items_data.length === 0) {\n console.log('❌ No items received from Supabase query.');\n console.log('→ Will generate new token.');\n return [\n {\n json: {\n tokenValid: false,\n reason: 'no_token_found',\n message: 'No token exists in database'\n }\n }\n ];\n}\n\nconst firstItem = items_data[0].json || {};\nconsole.log('First item keys:', Object.keys(firstItem));\n\n// Check if the item has actual token data\nif (Object.keys(firstItem).length === 0 || !firstItem.token) {\n console.log('❌ No token data found in Supabase result.');\n console.log('→ Will generate new token.');\n return [\n {\n json: {\n tokenValid: false,\n reason: 'no_token_found',\n message: 'Token field is empty or missing'\n }\n }\n ];\n}\n\nconst tokenRecord = firstItem;\nconst expiresAt = tokenRecord.expires_at;\n\n// Check if expires_at exists and is in the future\nif (!expiresAt) {\n console.log('⚠️ Token has no expiration date.');\n console.log('→ Will generate new token.');\n return [\n {\n json: {\n tokenValid: false,\n reason: 'no_expiration_date',\n message: 'Token missing expiration date',\n existingToken: tokenRecord\n }\n }\n ];\n}\n\n// Parse expiration date\nconst expirationDate = new Date(expiresAt);\nconst now = new Date();\nconst daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24));\n\nconsole.log('=== Token Expiration Check ===');\nconsole.log('Current time:', now.toISOString());\nconsole.log('Token expires:', expirationDate.toISOString());\nconsole.log('Days until expiration:', daysUntilExpiration);\n\n// If token expires in less than 30 days, regenerate\nif (daysUntilExpiration < 30) {\n console.log('⚠️ Token expires in less than 30 days.');\n console.log('→ Will generate new token.');\n return [\n {\n json: {\n tokenValid: false,\n reason: 'expiring_soon',\n message: `Token expires in ${daysUntilExpiration} days`,\n daysUntilExpiration: daysUntilExpiration,\n existingToken: tokenRecord\n }\n }\n ];\n}\n\n// Token expiration looks good, proceed to API validation\nconsole.log('✅ Token expiration date is valid (', daysUntilExpiration, 'days remaining)');\nconsole.log('→ Next: Testing token against NPM API...');\nreturn [\n {\n json: {\n tokenValid: true,\n reason: 'expiration_valid',\n message: `Token expires in ${daysUntilExpiration} days`,\n daysUntilExpiration: daysUntilExpiration,\n existingToken: tokenRecord\n }\n }\n];" }, "id": "validate-existing-token-node", "name": "Check Token Expiration", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 640, 400 ], "alwaysOutputData": true, "notes": "Step 1: Validates token expiration date. Handles all edge cases for empty/missing data. Always returns a clear tokenValid boolean for routing." }, { "parameters": { "url": "https://npm.dfw.ben.io/api/nginx/proxy-hosts", "options": { "timeout": 5000 }, "headerParametersUi": { "parameter": [ { "name": "Authorization", "value": "=Bearer {{ $json.existingToken.token }}" } ] } }, "id": "test-token-validity-node", "name": "Test Token Against NPM API", "type": "n8n-nodes-base.httpRequest", "typeVersion": 1, "position": [ 1040, 288 ], "notes": "Tests if the existing token actually works by making a test API call to NPM. Returns 200 if valid, 401 if invalid." }, { "parameters": { "jsCode": "// Check if the token actually works with NPM API\nconst response = $input.item.json;\nconst statusCode = response.statusCode || 200;\nconst previousValidation = $('Check Token Expiration').item.json;\n\nconsole.log('=== NPM Token API Test ===');\nconsole.log('API Response Status:', statusCode);\n\n// Token works if we get 200 OK\nif (statusCode === 200 || statusCode === 304) {\n console.log('✅ Token successfully validated against NPM API');\n console.log('Token expires in', previousValidation.daysUntilExpiration, 'days');\n return [{\n json: {\n tokenValid: true,\n apiVerified: true,\n daysUntilExpiration: previousValidation.daysUntilExpiration,\n existingToken: previousValidation.existingToken\n }\n }];\n}\n\n// Token is invalid (401 Unauthorized or other error)\nconsole.log('❌ Token validation failed against NPM API');\nconsole.log('Status code:', statusCode);\nreturn [{\n json: {\n tokenValid: false,\n apiVerified: false,\n reason: 'api_validation_failed',\n statusCode: statusCode,\n daysUntilExpiration: previousValidation.daysUntilExpiration\n }\n}];" }, "id": "evaluate-token-test-node", "name": "Evaluate API Test Result", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1248, 288 ], "notes": "Evaluates the API test response. If 200/304, token is valid. If 401 or other error, token needs regeneration." }, { "parameters": { "conditions": { "boolean": [ { "value1": "={{ $json.tokenValid }}", "value2": true } ] }, "options": {} }, "id": "route-based-on-validity-node", "name": "API Test Passed?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 1440, 400 ], "notes": "Routes based on API validation result. True = token valid and working. False = regenerate token." }, { "parameters": { "jsCode": "console.log('✅ Token fully validated and working!');\nconsole.log('- Expiration: Valid (', $json.daysUntilExpiration, 'days remaining)');\nconsole.log('- API Test: Passed');\nconsole.log('No action needed. Next check in ~11 months.');\n\nreturn [{\n json: {\n status: 'skipped',\n message: 'Existing token is valid and working',\n daysUntilExpiration: $json.daysUntilExpiration,\n expiresAt: $json.existingToken.expires_at,\n apiVerified: true\n }\n}];" }, "id": "token-still-valid-node", "name": "Token Valid - Skip", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1648, 288 ], "notes": "Token passed both expiration and API validation. No regeneration needed." }, { "parameters": { "method": "POST", "url": "https://npm.dfw.ben.io/api/tokens", "authentication": "genericCredentialType", "genericAuthType": "httpCustomAuth", "options": {} }, "id": "npm-login-node", "name": "NPM Login", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1168, 656 ], "credentials": { "httpCustomAuth": { "id": "7XEtlKn9X5YzJfiJ", "name": "NPM Admin Credentials" } }, "notes": "Authenticates with NPM using stored credentials to generate new token" }, { "parameters": { "url": "https://npm.dfw.ben.io/api/tokens?expiry=1y", "options": {}, "headerParametersUi": { "parameter": [ { "name": "Authorization", "value": "=Bearer {{ $json.token }}" } ] } }, "id": "get-long-lived-token-node", "name": "Get Long-Lived Token", "type": "n8n-nodes-base.httpRequest", "typeVersion": 1, "position": [ 1360, 656 ], "notes": "Requests a 1-year long-lived token" }, { "parameters": { "jsCode": "// Extract the long-lived token from the response\nconst token = $input.item.json.token;\nconst expiresAt = $input.item.json.expires;\n\nconsole.log('Generated new NPM long-lived token');\nconsole.log('Expires at:', expiresAt);\n\nreturn {\n json: {\n token: token,\n expiresAt: expiresAt,\n generatedAt: new Date().toISOString(),\n tokenPreview: token.substring(0, 20) + '...'\n }\n};" }, "id": "extract-token-node", "name": "Extract Token", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1568, 656 ], "notes": "Extracts and formats the token for storage" }, { "parameters": { "jsCode": "// Prepare the record for Supabase storage\nconst inputData = $input.item.json;\n\nreturn {\n json: {\n service_name: 'npm_dfw',\n token: inputData.token,\n expires_at: inputData.expiresAt,\n generated_at: inputData.generatedAt,\n last_updated: new Date().toISOString()\n }\n};" }, "id": "prepare-token-record", "name": "Prepare Token Record", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1760, 656 ], "notes": "Formats the token data for Supabase storage" }, { "parameters": { "jsCode": "// Check if UPDATE found and updated a row\nconst input = $input.item.json || {};\nconst hasData = Object.keys(input).length > 0;\n\nconsole.log('=== Checking UPDATE Result ===');\nconsole.log('Input item:', JSON.stringify(input, null, 2));\nconsole.log('Has data:', hasData);\nconsole.log('Keys:', Object.keys(input));\n\n// Check if we have the key database fields\nconst hasToken = input.token !== undefined && input.token !== null;\nconst hasServiceName = input.service_name !== undefined && input.service_name !== null;\n\nconsole.log('Has token field:', hasToken);\nconsole.log('Has service_name field:', hasServiceName);\n\n// If UPDATE returned token data, it succeeded\nif (hasData && (hasToken || hasServiceName)) {\n console.log('✅ UPDATE succeeded - row was found and updated');\n return { json: { ...input, needsCreate: false, operation: 'updated' } };\n}\n\n// UPDATE found no rows - need to CREATE\nconsole.log('⚠️ UPDATE found no rows or returned empty - will CREATE new row');\nconst tokenData = $('Prepare Token Record').item.json;\nreturn { json: { ...tokenData, needsCreate: true, operation: 'create_needed' } };" }, "id": "check-update-result", "name": "Did Update Succeed?", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2112, 656 ], "alwaysOutputData": true, "notes": "Checks if UPDATE succeeded. If no rows were updated, routes to CREATE." }, { "parameters": { "conditions": { "boolean": [ { "value1": "={{ $json.needsCreate }}", "value2": true } ] }, "options": {} }, "id": "route-create-check", "name": "Need to Create?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 2288, 656 ], "notes": "Routes to CREATE if UPDATE found no rows (first run). Otherwise skips to success." }, { "parameters": { "jsCode": "console.log('✅ NPM Token successfully refreshed and stored');\nconsole.log('Token expires at:', $input.item.json.expires_at || 'N/A');\nconsole.log('Operation:', $input.item.json.operation || 'completed');\nconsole.log('Next refresh in ~11 months or when token has <30 days remaining');\n\nreturn {\n json: {\n status: 'success',\n operation: 'token_refreshed',\n message: 'NPM long-lived token refreshed and stored in Supabase',\n expiresAt: $input.item.json.expires_at\n }\n};" }, "id": "success-notification", "name": "Log Success", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2800, 848 ] }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "leftValue": "={{ $json.tokenValid }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals" }, "id": "f484389b-d67b-4d23-a498-4efb7afe257c" } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Valid - Test API" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }, "conditions": [ { "leftValue": "={{ $json.tokenValid }}", "rightValue": false, "operator": { "type": "boolean", "operation": "equals" }, "id": "ad992289-38cb-42b1-8eff-ff14b3fa47c4" } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Invalid - Generate" } ] }, "options": { "fallbackOutput": "extra" } }, "id": "switch-token-status", "name": "Route By Token Status", "type": "n8n-nodes-base.switch", "typeVersion": 3, "position": [ 848, 400 ], "notes": "Routes based on tokenValid. Case 0: false=Generate. Case 1: true=Test API. Fallback case 2 for unexpected values." }, { "id": "check-existing-token-node", "name": "Check Existing Token (Postgres)", "type": "n8n-nodes-base.postgres", "typeVersion": 1, "position": [ 448, 400 ], "parameters": { "operation": "executeQuery", "query": "SELECT * FROM npm_tokens WHERE service_name = 'npm_dfw' LIMIT 1;" }, "credentials": { "postgres": { "id": "Ik8CFyap8ic2Md3M", "name": "n8n-infra" } } }, { "id": "Update Token in Supabase", "name": "Update Token in Postgres", "type": "n8n-nodes-base.postgres", "typeVersion": 1, "position": [ 2000, 400 ], "parameters": { "operation": "executeQuery", "options": { "queryParameterValues": "{{$json.token}},{{$json.expires_at}},{{$json.generated_at}},{{$json.last_updated}},{{$json.service_name}}" }, "query": "UPDATE npm_tokens SET token = $1, expires_at = $2, generated_at = $3, last_updated = $4 WHERE service_name = $5;" }, "credentials": { "postgres": { "id": "Ik8CFyap8ic2Md3M", "name": "n8n-infra" } } }, { "id": "Create Token in Supabase", "name": "Create Token in Postgres", "type": "n8n-nodes-base.postgres", "typeVersion": 1, "position": [ 2400, 500 ], "parameters": { "operation": "executeQuery", "options": { "queryParameterValues": "{{$json.service_name}},{{$json.token}},{{$json.expires_at}},{{$json.generated_at}},{{$json.last_updated}}" }, "query": "INSERT INTO npm_tokens (service_name, token, expires_at, generated_at, last_updated) VALUES ($1, $2, $3, $4, $5);" }, "credentials": { "postgres": { "id": "Ik8CFyap8ic2Md3M", "name": "n8n-infra" } } } ], "connections": { "Test Token Against NPM API": { "main": [ [ { "node": "Evaluate API Test Result", "type": "main", "index": 0 } ] ] }, "Evaluate API Test Result": { "main": [ [ { "node": "API Test Passed?", "type": "main", "index": 0 } ] ] }, "API Test Passed?": { "main": [ [ { "node": "Token Valid - Skip", "type": "main", "index": 0 } ], [ { "node": "NPM Login", "type": "main", "index": 0 } ] ] }, "NPM Login": { "main": [ [ { "node": "Get Long-Lived Token", "type": "main", "index": 0 } ] ] }, "Get Long-Lived Token": { "main": [ [ { "node": "Extract Token", "type": "main", "index": 0 } ] ] }, "Extract Token": { "main": [ [ { "node": "Prepare Token Record", "type": "main", "index": 0 } ] ] }, "Did Update Succeed?": { "main": [ [ { "node": "Need to Create?", "type": "main", "index": 0 } ] ] }, "Need to Create?": { "main": [ [ { "node": "Log Success", "type": "main", "index": 0 }, { "node": "Create Token in Postgres", "type": "main", "index": 0 } ] ] }, "Check Token Expiration": { "main": [ [ { "node": "Route By Token Status", "type": "main", "index": 0 } ] ] }, "Route By Token Status": { "main": [ [ { "node": "Test Token Against NPM API", "type": "main", "index": 0 } ], [ { "node": "NPM Login", "type": "main", "index": 0 } ], [ { "node": "NPM Login", "type": "main", "index": 0 } ] ] }, "Manual Trigger": { "main": [ [ { "node": "Check Existing Token (Postgres)", "type": "main", "index": 0 } ] ] }, "Schedule Token Refresh": { "main": [ [ { "node": "Check Existing Token (Postgres)", "type": "main", "index": 0 } ] ] }, "Check Existing Token (Postgres)": { "main": [ [ { "node": "Check Token Expiration", "type": "main", "index": 0 } ] ] }, "Prepare Token Record": { "main": [ [ { "node": "Update Token in Postgres", "type": "main", "index": 0 } ] ] }, "Update Token in Postgres": { "main": [ [ { "node": "Did Update Succeed?", "type": "main", "index": 0 } ] ] }, "Create Token in Postgres": { "main": [ [ { "node": "Log Success", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "callerPolicy": "workflowsFromSameOwner", "availableInMCP": false }, "triggerCount": 1, "versionId": "a927774e-18ea-41b2-b3da-9dee36c2b7d1", "owner": { "type": "personal", "projectId": "FeLO36wNUAcn61Wj", "projectName": "Ben W ", "personalEmail": "admin@ben.io" }, "parentFolderId": "of8yoeyjjIAhYdnQ", "isArchived": false }