{ "updatedAt": "2026-08-15T13:31:33.469Z", "createdAt": "2026-07-29T13:08:05.444Z", "id": "RaNkDfE2YCrZgCbx", "name": "GLM Hub - Solicitudes de acceso y aprobación", "description": null, "active": true, "isArchived": false, "nodes": [ { "parameters": { "httpMethod": "POST", "path": "glm-hub-solicitar-acceso", "responseMode": "responseNode", "options": { "allowedOrigins": "*" } }, "id": "6fee8ce5-2837-4683-9b9c-fd1022a87f5c", "name": "Webhook - Solicitar acceso", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -1920, 896 ], "webhookId": "ad3e98b9-baf2-400f-90e7-dcd4c5c20457" }, { "parameters": { "jsCode": "const source = $json.body ?? $json;\nlet payload = source;\nif (typeof payload === 'string') {\n try { payload = JSON.parse(payload); } catch { payload = {}; }\n}\nconst accessToken = String(payload?.accessToken ?? '').trim();\nconst appId = String(payload?.appId ?? '').trim();\nconst jwtPattern = /^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/;\nconst uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nlet error = '';\nif (!jwtPattern.test(accessToken)) error = 'La sesión recibida no es válida.';\nelse if (!uuidPattern.test(appId)) error = 'Selecciona una aplicación válida.';\nreturn [{ json: { valid: !error, error, authorization: `Bearer ${accessToken}`, appId } }];" }, "id": "05811eaf-72a8-448c-8c01-16b9d9d7a592", "name": "Validar solicitud de acceso", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -1696, 896 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "b5933422-7722-4c98-b34b-262a20d3ac05", "leftValue": "={{ $json.valid }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "fddbe870-eea1-4b94-9e64-c4aec9c4dad7", "name": "¿Solicitud de acceso válida?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ -1472, 896 ] }, { "parameters": { "respondWith": "json", "responseBody": "={{ { ok: false, error: $json.error } }}", "options": { "responseCode": 400, "responseHeaders": { "entries": [ { "name": "Cache-Control", "value": "no-store" } ] } } }, "id": "397ca441-991c-4f5d-9ce8-1df22a9fccc8", "name": "Responder solicitud inválida", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ -1152, 1056 ] }, { "parameters": { "method": "POST", "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_prepare_access_request", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "apikey", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" }, { "name": "Authorization", "value": "={{ $('Validar solicitud de acceso').first().json.authorization }}" }, { "name": "Accept", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ {\n p_app_id: $('Validar solicitud de acceso').first().json.appId\n} }}", "options": { "response": { "response": { "neverError": true, "responseFormat": "json" } }, "timeout": 30000 } }, "id": "1315636b-e124-4a59-9308-61f6855c482a", "name": "Crear solicitud en Supabase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ -688, 816 ] }, { "parameters": { "jsCode": "const source = $input.first()?.json ?? {};\n\nconst parseJson = (value) => {\n if (typeof value !== 'string') return value;\n\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\nlet payload =\n source.body ??\n source.data ??\n source.response?.body ??\n source;\n\npayload = parseJson(payload);\n\nif (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n}\n\nif (\n payload &&\n typeof payload === 'object' &&\n payload.body !== undefined &&\n payload.ok === undefined\n) {\n payload = parseJson(payload.body);\n\n if (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n }\n}\n\nif (payload?.ok !== true) {\n return [\n {\n json: {\n created: false,\n publicError:\n 'La solicitud fue registrada, pero no se pudo enviar el correo. Intenta nuevamente en unos minutos.',\n technicalError: String(\n payload?.error ??\n payload?.message ??\n 'No se pudieron emitir los enlaces de aprobación.'\n )\n }\n }\n ];\n}\n\nconst escapeHtml = (value) =>\n String(value ?? '').replace(\n /[&<>\"']/g,\n (c) =>\n ({\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n })[c]\n );\n\nconst encode = encodeURIComponent;\n\nconst confirmBase =\n 'https://agenteit.digitalcompass.agency/webhook/glm-hub-confirmar-solicitud';\n\nconst requesterName = String(\n payload.requesterName ?? 'Usuario GLM'\n);\n\nconst requesterEmail = String(\n payload.requesterEmail ?? ''\n);\n\nconst appName = String(\n payload.appName ?? 'Aplicación GLM'\n);\n\nconst appCategory = String(\n payload.appCategory ?? ''\n);\n\nconst requestId = String(\n payload.requestId ?? ''\n);\n\nconst approvers = Array.isArray(payload.approvers)\n ? payload.approvers\n : [];\n\nif (approvers.length < 2) {\n return [\n {\n json: {\n created: false,\n publicError:\n 'No se pudo completar el envío de la solicitud. Intenta nuevamente en unos minutos.',\n technicalError: `Se esperaban al menos 2 aprobadores y se recibieron ${approvers.length}.`\n }\n }\n ];\n}\n\nreturn approvers.map((approver) => {\n const approveUrl =\n `${confirmBase}?token=${encode(\n String(approver.token ?? '')\n )}&decision=approved&app=${encode(appName)}`;\n\n const rejectUrl =\n `${confirmBase}?token=${encode(\n String(approver.token ?? '')\n )}&decision=rejected&app=${encode(appName)}`;\n\n const html = `\n\n\n\n\n\n \n \n\n \n\n \n \n \n \n \n\n \n \n\n \n GLM HUB · SOLICITUD DE ACCESO\n \n\n \n Una persona necesita acceso.\n \n\n \n Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.\n

\n\n \n \n\n \n \n\n \n\n \n \n Solicitante\n \n\n \n ${escapeHtml(requesterName)}\n \n \n\n \n \n Correo\n \n\n \n ${escapeHtml(requesterEmail)}\n \n \n\n \n \n Aplicación\n \n\n \n ${escapeHtml(appName)}\n \n \n\n \n \n Área\n \n\n \n ${escapeHtml(appCategory)}\n \n \n\n \n\n \n \n\n \n \n\n \n Por seguridad, cada botón abre una pantalla de confirmación.\n La primera decisión confirmada será definitiva para todos los aprobadores.\n

\n\n \n \n\n \n \n Aceptar solicitud\n \n \n\n \n \n Rechazar solicitud\n \n \n\n \n \n\n \n \n\n \n \n GomezLee Marketing · Uso interno\n \n \n\n \n\n \n \n\n\n\n`;\n\n return {\n json: {\n created: true,\n requestId,\n sendTo: String(approver.email ?? ''),\n subject: `Solicitud de acceso | ${appName} | ${requesterName}`,\n html\n }\n };\n});" }, "id": "b38bcf45-4f58-4997-87e2-247c6478bdc2", "name": "Preparar correos de aprobación", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 448, 768 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "02a636e0-63a0-4a56-b108-65029e555646", "leftValue": "={{ $json.created }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "d1c724f7-4048-4545-b055-55d355fbe4bc", "name": "¿Solicitud registrada?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 688, 768 ] }, { "parameters": { "sendTo": "={{ $json.sendTo }}", "subject": "={{ $json.subject }}", "message": "={{ $json.html }}", "options": { "appendAttribution": false, "senderName": "GLM Hub" } }, "id": "b1a5605e-0ad9-4cae-8290-dec2427f645f", "name": "Enviar solicitud a aprobadores", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ 928, 672 ], "webhookId": "7dc65e7b-4fb6-4a2f-b685-34171dd9ce1a", "credentials": { "gmailOAuth2": { "id": "UDcO1FLJqA453V2D", "name": "Gmail account 3" } } }, { "parameters": { "respondWith": "json", "responseBody": "={{ { ok: true, message: 'La solicitud fue enviada correctamente a las personas responsables de aprobación.', requestId: $('Preparar correos de aprobación').first().json.requestId } }}", "options": { "responseCode": 200, "responseHeaders": { "entries": [ { "name": "Cache-Control", "value": "no-store" } ] } } }, "id": "8280f9b8-18ee-456c-8e71-2c923f8e60ec", "name": "Responder solicitud enviada", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 1168, 672 ] }, { "parameters": { "respondWith": "json", "responseBody": "={{ {\n ok: false,\n error: $json.publicError || 'No pudimos enviar la solicitud. Intenta nuevamente.'\n} }}", "options": { "responseCode": 409, "responseHeaders": { "entries": [ { "name": "Cache-Control", "value": "no-store" } ] } } }, "id": "59696c39-a80c-4548-80a8-162d63ea1346", "name": "Responder error al registrar", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 928, 880 ] }, { "parameters": { "path": "glm-hub-confirmar-solicitud", "responseMode": "responseNode", "options": {} }, "id": "e0152ea1-39a8-49e1-add4-ec5f83b7b288", "name": "Webhook - Confirmar decisión", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -560, 1600 ], "webhookId": "cba1c38c-37bc-4acb-bd61-48035604a5c4" }, { "parameters": { "jsCode": "const query = $json.query ?? {};\nconst token = String(query.token ?? '').trim();\nconst decision = String(query.decision ?? '').trim().toLowerCase();\nconst appName = String(query.app ?? 'aplicación GLM').trim();\nconst valid = /^[0-9a-f]{64}$/i.test(token) && ['approved','rejected'].includes(decision);\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst actionUrl = 'https://agenteit.digitalcompass.agency/webhook/glm-hub-resolver-solicitud';\nconst approved = decision === 'approved';\nconst title = approved ? 'Confirmar aprobación' : 'Confirmar rechazo';\nconst actionLabel = approved ? 'Sí, aprobar solicitud' : 'Sí, rechazar solicitud';\nconst actionColor = approved ? '#6CC24A' : '#E87722';\nconst html = valid\n ? `${title} · GLM Hub
\"GomezLee

GLM HUB · DECISIÓN DE ACCESO

${title}

Vas a ${approved ? 'aprobar' : 'rechazar'} la solicitud de acceso a ${escapeHtml(appName)}. La decisión será definitiva y los otros enlaces quedarán sin efecto.

Cierra esta pestaña para cancelar sin registrar ninguna decisión.

`\n : `
\"GomezLee

Enlace no válido

El enlace está incompleto o no corresponde a una solicitud de GLM Hub.

`;\nreturn [{ json: { html } }];" }, "id": "1cafd88f-b081-45fe-ab11-9de66d2337e0", "name": "Construir página de confirmación", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -336, 1600 ] }, { "parameters": { "respondWith": "text", "responseBody": "={{ $json.html }}", "options": { "responseCode": 200, "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "text/html; charset=utf-8" }, { "name": "Cache-Control", "value": "no-store" }, { "name": "X-Frame-Options", "value": "DENY" } ] } } }, "id": "7827c03c-dae8-4b9b-9c31-7f2a3a1e7dfe", "name": "Mostrar confirmación", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ -112, 1600 ] }, { "parameters": { "httpMethod": "POST", "path": "glm-hub-resolver-solicitud", "responseMode": "responseNode", "options": {} }, "id": "1cc0149c-206d-479e-b432-d9060bebb79d", "name": "Webhook - Resolver solicitud", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -1536, 2288 ], "webhookId": "54eeee1b-b24b-46ae-84dc-70c4103e3a3a" }, { "parameters": { "jsCode": "const source = $json.body ?? $json;\nlet payload = source;\nif (typeof payload === 'string') {\n try {\n payload = Object.fromEntries(new URLSearchParams(payload));\n } catch { payload = {}; }\n}\nconst token = String(payload?.token ?? '').trim();\nconst decision = String(payload?.decision ?? '').trim().toLowerCase();\nlet error = '';\nif (!/^[0-9a-f]{64}$/i.test(token)) error = 'El enlace de decisión no es válido.';\nelse if (!['approved','rejected'].includes(decision)) error = 'La decisión recibida no es válida.';\nreturn [{ json: { valid: !error, error, token, decision } }];" }, "id": "864d54fe-72f0-488a-bff6-0de0d9044308", "name": "Validar decisión", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -1312, 2288 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "5c42c35a-903b-4a18-a7ef-aab6e6bb5c26", "leftValue": "={{ $json.valid }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "07f79dcf-3d55-4122-9713-f3a1009e4657", "name": "¿Decisión válida?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ -1088, 2288 ] }, { "parameters": { "jsCode": "const message = String($json.error ?? 'El enlace de decisión no es válido.');\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst html = `
\"GomezLee

No se pudo procesar

${escapeHtml(message)}

`;\nreturn [{ json: { html } }];" }, "id": "da7d60fb-beb0-4a3e-94e7-31cd643b40c7", "name": "Preparar decisión inválida", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -864, 2480 ] }, { "parameters": { "respondWith": "text", "responseBody": "={{ $json.html }}", "options": { "responseCode": 400, "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "text/html; charset=utf-8" }, { "name": "Cache-Control", "value": "no-store" }, { "name": "X-Frame-Options", "value": "DENY" } ] } } }, "id": "1154375e-9680-4582-8bdd-514b15d27726", "name": "Responder decisión inválida", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ -624, 2480 ] }, { "parameters": { "method": "POST", "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_decide_access_request", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "apikey", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Accept", "value": "application/json" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ {\n p_token: $('Validar decisión').first().json.token,\n p_decision: $('Validar decisión').first().json.decision\n} }}", "options": { "response": { "response": { "neverError": true, "responseFormat": "json" } }, "timeout": 30000 } }, "id": "f453b62b-82ef-49e1-8b7a-e50a51a14fa3", "name": "Registrar decisión en Supabase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ -864, 2208 ] }, { "parameters": { "jsCode": "const source = $input.first()?.json ?? {};\nlet payload = source.body ?? source;\nif (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch { payload = {}; } }\nif (Array.isArray(payload)) payload = payload[0] ?? {};\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst status = String(payload.status ?? '');\nconst approved = status === 'approved';\nconst statusLabel = approved ? 'APROBADA' : status === 'rejected' ? 'RECHAZADA' : 'SIN PROCESAR';\nconst statusColor = approved ? '#6CC24A' : '#E87722';\nconst ok = payload.ok === true;\nconst alreadyDecided = payload.alreadyDecided === true;\nconst requesterName = String(payload.requesterName ?? '');\nconst requesterEmail = String(payload.requesterEmail ?? '');\nconst appName = String(payload.appName ?? 'Aplicación GLM');\nconst decidedByName = String(payload.decidedByName ?? '');\nconst decidedByEmail = String(payload.decidedByEmail ?? '');\nconst responseTitle = ok ? (approved ? 'Solicitud aprobada' : 'Solicitud rechazada') : alreadyDecided ? 'Solicitud ya atendida' : 'No se pudo procesar';\nconst responseMessage = ok\n ? `La decisión fue registrada. IT Support recibirá los datos para continuar con el acceso.`\n : alreadyDecided\n ? `Esta solicitud ya fue ${status === 'approved' ? 'aprobada' : 'rechazada'} por ${escapeHtml(decidedByName || decidedByEmail || 'otra persona')}. No se realizó ningún cambio adicional.`\n : escapeHtml(payload.error ?? 'El enlace no pudo ser validado.');\nconst responseHtml = `${responseTitle} · GLM Hub
\"GomezLee

GLM HUB · SOLICITUD DE ACCESO

${responseTitle}

${responseMessage}

Ya puedes cerrar esta pestaña.

`;\nconst itHtml = ok ? `
\"GomezLee
GLM HUB · DECISIÓN REGISTRADA

Solicitud ${statusLabel.toLowerCase()}.

IT Support debe continuar según la decisión indicada.

${statusLabel}
Persona solicitante${escapeHtml(requesterName)}
Correo solicitante${escapeHtml(requesterEmail)}
Aplicación${escapeHtml(appName)}
Decisión${statusLabel}
Decidido por${escapeHtml(decidedByName)}
Correo de quien decidió${escapeHtml(decidedByEmail)}
GomezLee Marketing · GLM Hub · Uso interno
` : '';\nreturn [{ json: { notifyIt: ok, responseHtml, itHtml, subject: `Acceso ${approved ? 'aprobado' : 'rechazado'} | ${appName} | ${requesterName}` } }];" }, "id": "6913ff01-1482-4413-964f-eff10fa4a64e", "name": "Preparar resultado de decisión", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 352, 2224 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "2ef854a5-d270-49bf-9a6a-5bb92aeab434", "leftValue": "={{ $json.notifyIt }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "343ab147-11df-4030-96b9-b750e6151100", "name": "¿Notificar a IT Support?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 592, 2224 ] }, { "parameters": { "sendTo": "itsupport@gomezleemarketing.com", "subject": "={{ $json.subject }}", "message": "={{ $json.itHtml }}", "options": { "appendAttribution": false, "senderName": "GLM Hub" } }, "id": "c1be686a-a5a6-4285-abbd-bf9ed90247d0", "name": "Enviar decisión a IT Support", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ 832, 2128 ], "webhookId": "234c8e3b-b904-41b8-8a74-9ca835f759df", "credentials": { "gmailOAuth2": { "id": "UDcO1FLJqA453V2D", "name": "Gmail account 3" } } }, { "parameters": { "respondWith": "text", "responseBody": "={{ $('Preparar resultado de decisión').first().json.responseHtml }}", "options": { "responseCode": 200, "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "text/html; charset=utf-8" }, { "name": "Cache-Control", "value": "no-store" }, { "name": "X-Frame-Options", "value": "DENY" } ] } } }, "id": "f3f941b3-b902-4ed6-abd4-ec3e00baee36", "name": "Mostrar resultado final", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 1072, 2224 ] }, { "parameters": { "jsCode": "const source = $input.first()?.json ?? {};\n\nconst parseJson = (value) => {\n if (typeof value !== 'string') {\n return value;\n }\n\n try {\n return JSON.parse(value);\n } catch {\n return {};\n }\n};\n\nlet payload = source;\n\n// Compatibilidad por si n8n todavía entrega algún envoltorio.\nif (source.body !== undefined) {\n payload = source.body;\n} else if (source.data !== undefined) {\n payload = source.data;\n}\n\npayload = parseJson(payload);\n\nif (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n}\n\nconst registered = payload?.ok === true;\nconst code = String(payload?.code ?? '').toUpperCase();\nconst internalMessage = String(\n payload?.error ??\n payload?.message ??\n ''\n).trim();\n\nconst normalizedMessage = internalMessage.toLowerCase();\n\nlet publicError = 'No pudimos enviar la solicitud. Intenta nuevamente.';\n\nif (\n code === 'PENDING_EXISTS' ||\n normalizedMessage.includes('solicitud pendiente')\n) {\n publicError = 'Ya tienes una solicitud pendiente para esta aplicación.';\n} else if (\n normalizedMessage.includes('sesión') ||\n normalizedMessage.includes('jwt')\n) {\n publicError = 'Tu sesión venció. Cierra sesión e inicia nuevamente.';\n} else if (\n normalizedMessage.includes('administradores no necesitan')\n) {\n publicError =\n 'Tu cuenta administrativa no necesita solicitar acceso desde el Hub.';\n} else if (\n normalizedMessage.includes('no está autorizado')\n) {\n publicError =\n 'Tu cuenta no está habilitada para enviar solicitudes de acceso.';\n} else if (\n normalizedMessage.includes('ya no está publicada')\n) {\n publicError =\n 'La aplicación seleccionada ya no está disponible.';\n}\n\nreturn [\n {\n json: {\n registered,\n publicError,\n internalMessage,\n requestId: String(payload?.requestId ?? ''),\n requesterName: String(payload?.requesterName ?? ''),\n requesterEmail: String(payload?.requesterEmail ?? ''),\n appName: String(payload?.appName ?? ''),\n appCategory: String(payload?.appCategory ?? '')\n }\n }\n];" }, "id": "5e121030-2188-415d-8013-a54d58fedc91", "name": "Confirmar registro de solicitud", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -480, 816 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "aa091e1c-09f8-4cdb-b8ac-3cbc4a7a33c8", "leftValue": "={{ $json.registered }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "f722416a-53e0-403d-a301-8bfe4d43de60", "name": "¿Solicitud registrada en Supabase?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ -256, 816 ] }, { "parameters": { "method": "POST", "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_issue_access_request_tokens", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "apikey", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Accept", "value": "application/json" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ {\n p_request_id: $('Confirmar registro de solicitud').first().json.requestId\n} }}", "options": { "response": { "response": { "neverError": true, "responseFormat": "json" } }, "timeout": 30000 } }, "id": "dfe96316-ab43-49e2-b9fb-384d34c5389c", "name": "Emitir enlaces de aprobación", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 208, 768 ] }, { "parameters": { "content": "## 1. RECEPCIÓN Y VALIDACIÓN\n\nEste bloque recibe desde GLM Hub una nueva solicitud de acceso y valida que la información obligatoria sea correcta.\n\n- Verifica los datos del usuario y de la aplicación solicitada.\n- Rechaza inmediatamente solicitudes incompletas o inválidas.\n- Solo las solicitudes válidas continúan hacia el registro en Supabase.", "height": 560, "width": 1104, "color": 6 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -2016, 688 ], "id": "97451dbc-cd46-40e8-911b-7c4cf3e648f4", "name": "Sticky Note" }, { "parameters": { "content": "## 2. REGISTRO DE LA SOLICITUD\n\nGuarda la solicitud de acceso en Supabase y confirma que el registro se haya creado correctamente.\n\n- Almacena la información de la solicitud y su estado inicial.\n- Recupera y normaliza los datos devueltos por Supabase.\n- Solo permite continuar cuando existe una confirmación válida del registro.", "height": 384, "width": 736, "color": 5 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -800, 640 ], "id": "5f36aafe-12ce-431d-90e8-2b49cf214f1d", "name": "Sticky Note1" }, { "parameters": { "content": "## 3. ENVÍO A LOS APROBADORES\n\nGenera los enlaces de decisión, prepara los correos y notifica a las personas responsables de aprobar o rechazar la solicitud.\n\n- Mantiene como aprobadores base a Isaac Aracena, José Leopoldo Gómez y Máximo Gómez.\n- Agrega automáticamente al jefe inmediato del solicitante cuando existe `supervisor_email` en `empleados_glm`.\n- Evita duplicados cuando el jefe ya es uno de los aprobadores base.\n- Construye enlaces únicos para la aprobación y el rechazo.\n- Envía un correo personalizado a cada aprobador mediante Gmail.\n- La primera decisión registrada continúa siendo definitiva para todos los enlaces.", "height": 560, "width": 1264, "color": "#357C27" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 112, 480 ], "id": "bab6dbac-ae4b-46ee-b31e-e894aa1fc20a", "name": "Sticky Note2" }, { "parameters": { "content": "## 4. CONFIRMACIÓN DE LA DECISIÓN\n\nRecibe al aprobador desde el enlace enviado por correo y muestra una página previa antes de registrar la decisión definitiva.\n\n- Lee los parámetros incluidos en el enlace de aprobación o rechazo.\n- Construye una página con los datos de la solicitud.\n- Solicita una confirmación final para evitar decisiones accidentales.", "height": 352, "width": 1062, "color": "#8F622D" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -800, 1424 ], "id": "9115ee9e-a2ce-419f-a783-62cfe268c8fd", "name": "Sticky Note3" }, { "parameters": { "content": "## 5. VALIDACIÓN Y REGISTRO DE LA DECISIÓN\n\nValida la decisión confirmada por el aprobador y actualiza la solicitud correspondiente en Supabase.\n\n- Comprueba que la solicitud, el enlace y la decisión sean válidos.\n- Registra en Supabase la aprobación o el rechazo.\n- Impide procesar decisiones incorrectas, incompletas o manipuladas.\n- Muestra una respuesta controlada cuando la decisión no puede ser aceptada.", "height": 624, "width": 1184, "color": "#52500F" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -1616, 2032 ], "id": "68fac86e-bbee-4648-b2ed-bfe3b9e9829c", "name": "Sticky Note4" }, { "parameters": { "content": "## 6. NOTIFICACIÓN Y RESULTADO FINAL\n\nPrepara el resultado de la decisión, determina si se necesita intervención de IT Support y finaliza el proceso.\n\n- Construye el mensaje final de aprobación o rechazo.\n- Evalúa si la decisión debe notificarse al equipo de IT.\n- Envía por Gmail la información necesaria cuando corresponde.\n- Muestra al aprobador el resultado definitivo de la operación.", "height": 432, "width": 1104, "color": "#276D4F" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 192, 1968 ], "id": "024b61e1-bbc8-4838-9afe-7ee36445d0db", "name": "Sticky Note5" } ], "connections": { "Webhook - Solicitar acceso": { "main": [ [ { "node": "Validar solicitud de acceso", "type": "main", "index": 0 } ] ] }, "Validar solicitud de acceso": { "main": [ [ { "node": "¿Solicitud de acceso válida?", "type": "main", "index": 0 } ] ] }, "¿Solicitud de acceso válida?": { "main": [ [ { "node": "Crear solicitud en Supabase", "type": "main", "index": 0 } ], [ { "node": "Responder solicitud inválida", "type": "main", "index": 0 } ] ] }, "Crear solicitud en Supabase": { "main": [ [ { "node": "Confirmar registro de solicitud", "type": "main", "index": 0 } ] ] }, "Preparar correos de aprobación": { "main": [ [ { "node": "¿Solicitud registrada?", "type": "main", "index": 0 } ] ] }, "¿Solicitud registrada?": { "main": [ [ { "node": "Enviar solicitud a aprobadores", "type": "main", "index": 0 } ], [ { "node": "Responder error al registrar", "type": "main", "index": 0 } ] ] }, "Enviar solicitud a aprobadores": { "main": [ [ { "node": "Responder solicitud enviada", "type": "main", "index": 0 } ] ] }, "Webhook - Confirmar decisión": { "main": [ [ { "node": "Construir página de confirmación", "type": "main", "index": 0 } ] ] }, "Construir página de confirmación": { "main": [ [ { "node": "Mostrar confirmación", "type": "main", "index": 0 } ] ] }, "Webhook - Resolver solicitud": { "main": [ [ { "node": "Validar decisión", "type": "main", "index": 0 } ] ] }, "Validar decisión": { "main": [ [ { "node": "¿Decisión válida?", "type": "main", "index": 0 } ] ] }, "¿Decisión válida?": { "main": [ [ { "node": "Registrar decisión en Supabase", "type": "main", "index": 0 } ], [ { "node": "Preparar decisión inválida", "type": "main", "index": 0 } ] ] }, "Preparar decisión inválida": { "main": [ [ { "node": "Responder decisión inválida", "type": "main", "index": 0 } ] ] }, "Registrar decisión en Supabase": { "main": [ [ { "node": "Preparar resultado de decisión", "type": "main", "index": 0 } ] ] }, "Preparar resultado de decisión": { "main": [ [ { "node": "¿Notificar a IT Support?", "type": "main", "index": 0 } ] ] }, "¿Notificar a IT Support?": { "main": [ [ { "node": "Enviar decisión a IT Support", "type": "main", "index": 0 } ], [ { "node": "Mostrar resultado final", "type": "main", "index": 0 } ] ] }, "Enviar decisión a IT Support": { "main": [ [ { "node": "Mostrar resultado final", "type": "main", "index": 0 } ] ] }, "Confirmar registro de solicitud": { "main": [ [ { "node": "¿Solicitud registrada en Supabase?", "type": "main", "index": 0 } ] ] }, "¿Solicitud registrada en Supabase?": { "main": [ [ { "node": "Emitir enlaces de aprobación", "type": "main", "index": 0 } ], [ { "node": "Responder error al registrar", "type": "main", "index": 0 } ] ] }, "Emitir enlaces de aprobación": { "main": [ [ { "node": "Preparar correos de aprobación", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "binaryMode": "separate", "availableInMCP": true }, "staticData": null, "meta": { "templateCredsSetupCompleted": true }, "versionId": "f0bf0a23-7940-4d51-b12c-00ae0fd7cc50", "activeVersionId": "f0bf0a23-7940-4d51-b12c-00ae0fd7cc50", "versionCounter": 149, "triggerCount": 3, "shared": [ { "updatedAt": "2026-07-29T13:08:05.454Z", "createdAt": "2026-07-29T13:08:05.454Z", "role": "workflow:owner", "workflowId": "RaNkDfE2YCrZgCbx", "projectId": "PJpTANzTXIFibWsW", "project": { "updatedAt": "2026-04-22T14:25:09.686Z", "createdAt": "2026-04-22T14:22:54.790Z", "id": "PJpTANzTXIFibWsW", "name": "Isaac Aracena ", "type": "personal", "icon": null, "description": null, "creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" } } ], "tags": [], "activeVersion": { "updatedAt": "2026-08-15T13:32:14.000Z", "createdAt": "2026-08-15T13:31:33.472Z", "versionId": "f0bf0a23-7940-4d51-b12c-00ae0fd7cc50", "workflowId": "RaNkDfE2YCrZgCbx", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "glm-hub-solicitar-acceso", "responseMode": "responseNode", "options": { "allowedOrigins": "*" } }, "id": "6fee8ce5-2837-4683-9b9c-fd1022a87f5c", "name": "Webhook - Solicitar acceso", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -1920, 896 ], "webhookId": "ad3e98b9-baf2-400f-90e7-dcd4c5c20457" }, { "parameters": { "jsCode": "const source = $json.body ?? $json;\nlet payload = source;\nif (typeof payload === 'string') {\n try { payload = JSON.parse(payload); } catch { payload = {}; }\n}\nconst accessToken = String(payload?.accessToken ?? '').trim();\nconst appId = String(payload?.appId ?? '').trim();\nconst jwtPattern = /^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/;\nconst uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nlet error = '';\nif (!jwtPattern.test(accessToken)) error = 'La sesión recibida no es válida.';\nelse if (!uuidPattern.test(appId)) error = 'Selecciona una aplicación válida.';\nreturn [{ json: { valid: !error, error, authorization: `Bearer ${accessToken}`, appId } }];" }, "id": "05811eaf-72a8-448c-8c01-16b9d9d7a592", "name": "Validar solicitud de acceso", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -1696, 896 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "b5933422-7722-4c98-b34b-262a20d3ac05", "leftValue": "={{ $json.valid }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "fddbe870-eea1-4b94-9e64-c4aec9c4dad7", "name": "¿Solicitud de acceso válida?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ -1472, 896 ] }, { "parameters": { "respondWith": "json", "responseBody": "={{ { ok: false, error: $json.error } }}", "options": { "responseCode": 400, "responseHeaders": { "entries": [ { "name": "Cache-Control", "value": "no-store" } ] } } }, "id": "397ca441-991c-4f5d-9ce8-1df22a9fccc8", "name": "Responder solicitud inválida", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ -1152, 1056 ] }, { "parameters": { "method": "POST", "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_prepare_access_request", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "apikey", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" }, { "name": "Authorization", "value": "={{ $('Validar solicitud de acceso').first().json.authorization }}" }, { "name": "Accept", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ {\n p_app_id: $('Validar solicitud de acceso').first().json.appId\n} }}", "options": { "response": { "response": { "neverError": true, "responseFormat": "json" } }, "timeout": 30000 } }, "id": "1315636b-e124-4a59-9308-61f6855c482a", "name": "Crear solicitud en Supabase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ -688, 816 ] }, { "parameters": { "jsCode": "const source = $input.first()?.json ?? {};\n\nconst parseJson = (value) => {\n if (typeof value !== 'string') return value;\n\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\nlet payload =\n source.body ??\n source.data ??\n source.response?.body ??\n source;\n\npayload = parseJson(payload);\n\nif (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n}\n\nif (\n payload &&\n typeof payload === 'object' &&\n payload.body !== undefined &&\n payload.ok === undefined\n) {\n payload = parseJson(payload.body);\n\n if (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n }\n}\n\nif (payload?.ok !== true) {\n return [\n {\n json: {\n created: false,\n publicError:\n 'La solicitud fue registrada, pero no se pudo enviar el correo. Intenta nuevamente en unos minutos.',\n technicalError: String(\n payload?.error ??\n payload?.message ??\n 'No se pudieron emitir los enlaces de aprobación.'\n )\n }\n }\n ];\n}\n\nconst escapeHtml = (value) =>\n String(value ?? '').replace(\n /[&<>\"']/g,\n (c) =>\n ({\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n })[c]\n );\n\nconst encode = encodeURIComponent;\n\nconst confirmBase =\n 'https://agenteit.digitalcompass.agency/webhook/glm-hub-confirmar-solicitud';\n\nconst requesterName = String(\n payload.requesterName ?? 'Usuario GLM'\n);\n\nconst requesterEmail = String(\n payload.requesterEmail ?? ''\n);\n\nconst appName = String(\n payload.appName ?? 'Aplicación GLM'\n);\n\nconst appCategory = String(\n payload.appCategory ?? ''\n);\n\nconst requestId = String(\n payload.requestId ?? ''\n);\n\nconst approvers = Array.isArray(payload.approvers)\n ? payload.approvers\n : [];\n\nif (approvers.length < 2) {\n return [\n {\n json: {\n created: false,\n publicError:\n 'No se pudo completar el envío de la solicitud. Intenta nuevamente en unos minutos.',\n technicalError: `Se esperaban al menos 2 aprobadores y se recibieron ${approvers.length}.`\n }\n }\n ];\n}\n\nreturn approvers.map((approver) => {\n const approveUrl =\n `${confirmBase}?token=${encode(\n String(approver.token ?? '')\n )}&decision=approved&app=${encode(appName)}`;\n\n const rejectUrl =\n `${confirmBase}?token=${encode(\n String(approver.token ?? '')\n )}&decision=rejected&app=${encode(appName)}`;\n\n const html = `\n\n\n\n\n\n \n \n\n \n\n \n \n \n \n \n\n \n \n\n \n GLM HUB · SOLICITUD DE ACCESO\n \n\n \n Una persona necesita acceso.\n \n\n \n Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.\n

\n\n \n \n\n \n \n\n \n\n \n \n Solicitante\n \n\n \n ${escapeHtml(requesterName)}\n \n \n\n \n \n Correo\n \n\n \n ${escapeHtml(requesterEmail)}\n \n \n\n \n \n Aplicación\n \n\n \n ${escapeHtml(appName)}\n \n \n\n \n \n Área\n \n\n \n ${escapeHtml(appCategory)}\n \n \n\n \n\n \n \n\n \n \n\n \n Por seguridad, cada botón abre una pantalla de confirmación.\n La primera decisión confirmada será definitiva para todos los aprobadores.\n

\n\n \n \n\n \n \n Aceptar solicitud\n \n \n\n \n \n Rechazar solicitud\n \n \n\n \n \n\n \n \n\n \n \n GomezLee Marketing · Uso interno\n \n \n\n \n\n \n \n\n\n\n`;\n\n return {\n json: {\n created: true,\n requestId,\n sendTo: String(approver.email ?? ''),\n subject: `Solicitud de acceso | ${appName} | ${requesterName}`,\n html\n }\n };\n});" }, "id": "b38bcf45-4f58-4997-87e2-247c6478bdc2", "name": "Preparar correos de aprobación", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 448, 768 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "02a636e0-63a0-4a56-b108-65029e555646", "leftValue": "={{ $json.created }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "d1c724f7-4048-4545-b055-55d355fbe4bc", "name": "¿Solicitud registrada?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 688, 768 ] }, { "parameters": { "sendTo": "={{ $json.sendTo }}", "subject": "={{ $json.subject }}", "message": "={{ $json.html }}", "options": { "appendAttribution": false, "senderName": "GLM Hub" } }, "id": "b1a5605e-0ad9-4cae-8290-dec2427f645f", "name": "Enviar solicitud a aprobadores", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ 928, 672 ], "webhookId": "7dc65e7b-4fb6-4a2f-b685-34171dd9ce1a", "credentials": { "gmailOAuth2": { "id": "UDcO1FLJqA453V2D", "name": "Gmail account 3" } } }, { "parameters": { "respondWith": "json", "responseBody": "={{ { ok: true, message: 'La solicitud fue enviada correctamente a las personas responsables de aprobación.', requestId: $('Preparar correos de aprobación').first().json.requestId } }}", "options": { "responseCode": 200, "responseHeaders": { "entries": [ { "name": "Cache-Control", "value": "no-store" } ] } } }, "id": "8280f9b8-18ee-456c-8e71-2c923f8e60ec", "name": "Responder solicitud enviada", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 1168, 672 ] }, { "parameters": { "respondWith": "json", "responseBody": "={{ {\n ok: false,\n error: $json.publicError || 'No pudimos enviar la solicitud. Intenta nuevamente.'\n} }}", "options": { "responseCode": 409, "responseHeaders": { "entries": [ { "name": "Cache-Control", "value": "no-store" } ] } } }, "id": "59696c39-a80c-4548-80a8-162d63ea1346", "name": "Responder error al registrar", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 928, 880 ] }, { "parameters": { "path": "glm-hub-confirmar-solicitud", "responseMode": "responseNode", "options": {} }, "id": "e0152ea1-39a8-49e1-add4-ec5f83b7b288", "name": "Webhook - Confirmar decisión", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -560, 1600 ], "webhookId": "cba1c38c-37bc-4acb-bd61-48035604a5c4" }, { "parameters": { "jsCode": "const query = $json.query ?? {};\nconst token = String(query.token ?? '').trim();\nconst decision = String(query.decision ?? '').trim().toLowerCase();\nconst appName = String(query.app ?? 'aplicación GLM').trim();\nconst valid = /^[0-9a-f]{64}$/i.test(token) && ['approved','rejected'].includes(decision);\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst actionUrl = 'https://agenteit.digitalcompass.agency/webhook/glm-hub-resolver-solicitud';\nconst approved = decision === 'approved';\nconst title = approved ? 'Confirmar aprobación' : 'Confirmar rechazo';\nconst actionLabel = approved ? 'Sí, aprobar solicitud' : 'Sí, rechazar solicitud';\nconst actionColor = approved ? '#6CC24A' : '#E87722';\nconst html = valid\n ? `${title} · GLM Hub
\"GomezLee

GLM HUB · DECISIÓN DE ACCESO

${title}

Vas a ${approved ? 'aprobar' : 'rechazar'} la solicitud de acceso a ${escapeHtml(appName)}. La decisión será definitiva y los otros enlaces quedarán sin efecto.

Cierra esta pestaña para cancelar sin registrar ninguna decisión.

`\n : `
\"GomezLee

Enlace no válido

El enlace está incompleto o no corresponde a una solicitud de GLM Hub.

`;\nreturn [{ json: { html } }];" }, "id": "1cafd88f-b081-45fe-ab11-9de66d2337e0", "name": "Construir página de confirmación", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -336, 1600 ] }, { "parameters": { "respondWith": "text", "responseBody": "={{ $json.html }}", "options": { "responseCode": 200, "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "text/html; charset=utf-8" }, { "name": "Cache-Control", "value": "no-store" }, { "name": "X-Frame-Options", "value": "DENY" } ] } } }, "id": "7827c03c-dae8-4b9b-9c31-7f2a3a1e7dfe", "name": "Mostrar confirmación", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ -112, 1600 ] }, { "parameters": { "httpMethod": "POST", "path": "glm-hub-resolver-solicitud", "responseMode": "responseNode", "options": {} }, "id": "1cc0149c-206d-479e-b432-d9060bebb79d", "name": "Webhook - Resolver solicitud", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -1536, 2288 ], "webhookId": "54eeee1b-b24b-46ae-84dc-70c4103e3a3a" }, { "parameters": { "jsCode": "const source = $json.body ?? $json;\nlet payload = source;\nif (typeof payload === 'string') {\n try {\n payload = Object.fromEntries(new URLSearchParams(payload));\n } catch { payload = {}; }\n}\nconst token = String(payload?.token ?? '').trim();\nconst decision = String(payload?.decision ?? '').trim().toLowerCase();\nlet error = '';\nif (!/^[0-9a-f]{64}$/i.test(token)) error = 'El enlace de decisión no es válido.';\nelse if (!['approved','rejected'].includes(decision)) error = 'La decisión recibida no es válida.';\nreturn [{ json: { valid: !error, error, token, decision } }];" }, "id": "864d54fe-72f0-488a-bff6-0de0d9044308", "name": "Validar decisión", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -1312, 2288 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "5c42c35a-903b-4a18-a7ef-aab6e6bb5c26", "leftValue": "={{ $json.valid }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "07f79dcf-3d55-4122-9713-f3a1009e4657", "name": "¿Decisión válida?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ -1088, 2288 ] }, { "parameters": { "jsCode": "const message = String($json.error ?? 'El enlace de decisión no es válido.');\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst html = `
\"GomezLee

No se pudo procesar

${escapeHtml(message)}

`;\nreturn [{ json: { html } }];" }, "id": "da7d60fb-beb0-4a3e-94e7-31cd643b40c7", "name": "Preparar decisión inválida", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -864, 2480 ] }, { "parameters": { "respondWith": "text", "responseBody": "={{ $json.html }}", "options": { "responseCode": 400, "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "text/html; charset=utf-8" }, { "name": "Cache-Control", "value": "no-store" }, { "name": "X-Frame-Options", "value": "DENY" } ] } } }, "id": "1154375e-9680-4582-8bdd-514b15d27726", "name": "Responder decisión inválida", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ -624, 2480 ] }, { "parameters": { "method": "POST", "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_decide_access_request", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "apikey", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Accept", "value": "application/json" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ {\n p_token: $('Validar decisión').first().json.token,\n p_decision: $('Validar decisión').first().json.decision\n} }}", "options": { "response": { "response": { "neverError": true, "responseFormat": "json" } }, "timeout": 30000 } }, "id": "f453b62b-82ef-49e1-8b7a-e50a51a14fa3", "name": "Registrar decisión en Supabase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ -864, 2208 ] }, { "parameters": { "jsCode": "const source = $input.first()?.json ?? {};\nlet payload = source.body ?? source;\nif (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch { payload = {}; } }\nif (Array.isArray(payload)) payload = payload[0] ?? {};\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst status = String(payload.status ?? '');\nconst approved = status === 'approved';\nconst statusLabel = approved ? 'APROBADA' : status === 'rejected' ? 'RECHAZADA' : 'SIN PROCESAR';\nconst statusColor = approved ? '#6CC24A' : '#E87722';\nconst ok = payload.ok === true;\nconst alreadyDecided = payload.alreadyDecided === true;\nconst requesterName = String(payload.requesterName ?? '');\nconst requesterEmail = String(payload.requesterEmail ?? '');\nconst appName = String(payload.appName ?? 'Aplicación GLM');\nconst decidedByName = String(payload.decidedByName ?? '');\nconst decidedByEmail = String(payload.decidedByEmail ?? '');\nconst responseTitle = ok ? (approved ? 'Solicitud aprobada' : 'Solicitud rechazada') : alreadyDecided ? 'Solicitud ya atendida' : 'No se pudo procesar';\nconst responseMessage = ok\n ? `La decisión fue registrada. IT Support recibirá los datos para continuar con el acceso.`\n : alreadyDecided\n ? `Esta solicitud ya fue ${status === 'approved' ? 'aprobada' : 'rechazada'} por ${escapeHtml(decidedByName || decidedByEmail || 'otra persona')}. No se realizó ningún cambio adicional.`\n : escapeHtml(payload.error ?? 'El enlace no pudo ser validado.');\nconst responseHtml = `${responseTitle} · GLM Hub
\"GomezLee

GLM HUB · SOLICITUD DE ACCESO

${responseTitle}

${responseMessage}

Ya puedes cerrar esta pestaña.

`;\nconst itHtml = ok ? `
\"GomezLee
GLM HUB · DECISIÓN REGISTRADA

Solicitud ${statusLabel.toLowerCase()}.

IT Support debe continuar según la decisión indicada.

${statusLabel}
Persona solicitante${escapeHtml(requesterName)}
Correo solicitante${escapeHtml(requesterEmail)}
Aplicación${escapeHtml(appName)}
Decisión${statusLabel}
Decidido por${escapeHtml(decidedByName)}
Correo de quien decidió${escapeHtml(decidedByEmail)}
GomezLee Marketing · GLM Hub · Uso interno
` : '';\nreturn [{ json: { notifyIt: ok, responseHtml, itHtml, subject: `Acceso ${approved ? 'aprobado' : 'rechazado'} | ${appName} | ${requesterName}` } }];" }, "id": "6913ff01-1482-4413-964f-eff10fa4a64e", "name": "Preparar resultado de decisión", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 352, 2224 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "2ef854a5-d270-49bf-9a6a-5bb92aeab434", "leftValue": "={{ $json.notifyIt }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "343ab147-11df-4030-96b9-b750e6151100", "name": "¿Notificar a IT Support?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 592, 2224 ] }, { "parameters": { "sendTo": "itsupport@gomezleemarketing.com", "subject": "={{ $json.subject }}", "message": "={{ $json.itHtml }}", "options": { "appendAttribution": false, "senderName": "GLM Hub" } }, "id": "c1be686a-a5a6-4285-abbd-bf9ed90247d0", "name": "Enviar decisión a IT Support", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ 832, 2128 ], "webhookId": "234c8e3b-b904-41b8-8a74-9ca835f759df", "credentials": { "gmailOAuth2": { "id": "UDcO1FLJqA453V2D", "name": "Gmail account 3" } } }, { "parameters": { "respondWith": "text", "responseBody": "={{ $('Preparar resultado de decisión').first().json.responseHtml }}", "options": { "responseCode": 200, "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "text/html; charset=utf-8" }, { "name": "Cache-Control", "value": "no-store" }, { "name": "X-Frame-Options", "value": "DENY" } ] } } }, "id": "f3f941b3-b902-4ed6-abd4-ec3e00baee36", "name": "Mostrar resultado final", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ 1072, 2224 ] }, { "parameters": { "jsCode": "const source = $input.first()?.json ?? {};\n\nconst parseJson = (value) => {\n if (typeof value !== 'string') {\n return value;\n }\n\n try {\n return JSON.parse(value);\n } catch {\n return {};\n }\n};\n\nlet payload = source;\n\n// Compatibilidad por si n8n todavía entrega algún envoltorio.\nif (source.body !== undefined) {\n payload = source.body;\n} else if (source.data !== undefined) {\n payload = source.data;\n}\n\npayload = parseJson(payload);\n\nif (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n}\n\nconst registered = payload?.ok === true;\nconst code = String(payload?.code ?? '').toUpperCase();\nconst internalMessage = String(\n payload?.error ??\n payload?.message ??\n ''\n).trim();\n\nconst normalizedMessage = internalMessage.toLowerCase();\n\nlet publicError = 'No pudimos enviar la solicitud. Intenta nuevamente.';\n\nif (\n code === 'PENDING_EXISTS' ||\n normalizedMessage.includes('solicitud pendiente')\n) {\n publicError = 'Ya tienes una solicitud pendiente para esta aplicación.';\n} else if (\n normalizedMessage.includes('sesión') ||\n normalizedMessage.includes('jwt')\n) {\n publicError = 'Tu sesión venció. Cierra sesión e inicia nuevamente.';\n} else if (\n normalizedMessage.includes('administradores no necesitan')\n) {\n publicError =\n 'Tu cuenta administrativa no necesita solicitar acceso desde el Hub.';\n} else if (\n normalizedMessage.includes('no está autorizado')\n) {\n publicError =\n 'Tu cuenta no está habilitada para enviar solicitudes de acceso.';\n} else if (\n normalizedMessage.includes('ya no está publicada')\n) {\n publicError =\n 'La aplicación seleccionada ya no está disponible.';\n}\n\nreturn [\n {\n json: {\n registered,\n publicError,\n internalMessage,\n requestId: String(payload?.requestId ?? ''),\n requesterName: String(payload?.requesterName ?? ''),\n requesterEmail: String(payload?.requesterEmail ?? ''),\n appName: String(payload?.appName ?? ''),\n appCategory: String(payload?.appCategory ?? '')\n }\n }\n];" }, "id": "5e121030-2188-415d-8013-a54d58fedc91", "name": "Confirmar registro de solicitud", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ -480, 816 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "aa091e1c-09f8-4cdb-b8ac-3cbc4a7a33c8", "leftValue": "={{ $json.registered }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "id": "f722416a-53e0-403d-a301-8bfe4d43de60", "name": "¿Solicitud registrada en Supabase?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ -256, 816 ] }, { "parameters": { "method": "POST", "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_issue_access_request_tokens", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "apikey", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Authorization", "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" }, { "name": "Accept", "value": "application/json" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ {\n p_request_id: $('Confirmar registro de solicitud').first().json.requestId\n} }}", "options": { "response": { "response": { "neverError": true, "responseFormat": "json" } }, "timeout": 30000 } }, "id": "dfe96316-ab43-49e2-b9fb-384d34c5389c", "name": "Emitir enlaces de aprobación", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 208, 768 ] }, { "parameters": { "content": "## 1. RECEPCIÓN Y VALIDACIÓN\n\nEste bloque recibe desde GLM Hub una nueva solicitud de acceso y valida que la información obligatoria sea correcta.\n\n- Verifica los datos del usuario y de la aplicación solicitada.\n- Rechaza inmediatamente solicitudes incompletas o inválidas.\n- Solo las solicitudes válidas continúan hacia el registro en Supabase.", "height": 560, "width": 1104, "color": 6 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -2016, 688 ], "id": "97451dbc-cd46-40e8-911b-7c4cf3e648f4", "name": "Sticky Note" }, { "parameters": { "content": "## 2. REGISTRO DE LA SOLICITUD\n\nGuarda la solicitud de acceso en Supabase y confirma que el registro se haya creado correctamente.\n\n- Almacena la información de la solicitud y su estado inicial.\n- Recupera y normaliza los datos devueltos por Supabase.\n- Solo permite continuar cuando existe una confirmación válida del registro.", "height": 384, "width": 736, "color": 5 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -800, 640 ], "id": "5f36aafe-12ce-431d-90e8-2b49cf214f1d", "name": "Sticky Note1" }, { "parameters": { "content": "## 3. ENVÍO A LOS APROBADORES\n\nGenera los enlaces de decisión, prepara los correos y notifica a las personas responsables de aprobar o rechazar la solicitud.\n\n- Mantiene como aprobadores base a Isaac Aracena, José Leopoldo Gómez y Máximo Gómez.\n- Agrega automáticamente al jefe inmediato del solicitante cuando existe `supervisor_email` en `empleados_glm`.\n- Evita duplicados cuando el jefe ya es uno de los aprobadores base.\n- Construye enlaces únicos para la aprobación y el rechazo.\n- Envía un correo personalizado a cada aprobador mediante Gmail.\n- La primera decisión registrada continúa siendo definitiva para todos los enlaces.", "height": 560, "width": 1264, "color": "#357C27" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 112, 480 ], "id": "bab6dbac-ae4b-46ee-b31e-e894aa1fc20a", "name": "Sticky Note2" }, { "parameters": { "content": "## 4. CONFIRMACIÓN DE LA DECISIÓN\n\nRecibe al aprobador desde el enlace enviado por correo y muestra una página previa antes de registrar la decisión definitiva.\n\n- Lee los parámetros incluidos en el enlace de aprobación o rechazo.\n- Construye una página con los datos de la solicitud.\n- Solicita una confirmación final para evitar decisiones accidentales.", "height": 352, "width": 1062, "color": "#8F622D" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -800, 1424 ], "id": "9115ee9e-a2ce-419f-a783-62cfe268c8fd", "name": "Sticky Note3" }, { "parameters": { "content": "## 5. VALIDACIÓN Y REGISTRO DE LA DECISIÓN\n\nValida la decisión confirmada por el aprobador y actualiza la solicitud correspondiente en Supabase.\n\n- Comprueba que la solicitud, el enlace y la decisión sean válidos.\n- Registra en Supabase la aprobación o el rechazo.\n- Impide procesar decisiones incorrectas, incompletas o manipuladas.\n- Muestra una respuesta controlada cuando la decisión no puede ser aceptada.", "height": 624, "width": 1184, "color": "#52500F" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -1616, 2032 ], "id": "68fac86e-bbee-4648-b2ed-bfe3b9e9829c", "name": "Sticky Note4" }, { "parameters": { "content": "## 6. NOTIFICACIÓN Y RESULTADO FINAL\n\nPrepara el resultado de la decisión, determina si se necesita intervención de IT Support y finaliza el proceso.\n\n- Construye el mensaje final de aprobación o rechazo.\n- Evalúa si la decisión debe notificarse al equipo de IT.\n- Envía por Gmail la información necesaria cuando corresponde.\n- Muestra al aprobador el resultado definitivo de la operación.", "height": 432, "width": 1104, "color": "#276D4F" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 192, 1968 ], "id": "024b61e1-bbc8-4838-9afe-7ee36445d0db", "name": "Sticky Note5" } ], "connections": { "Webhook - Solicitar acceso": { "main": [ [ { "node": "Validar solicitud de acceso", "type": "main", "index": 0 } ] ] }, "Validar solicitud de acceso": { "main": [ [ { "node": "¿Solicitud de acceso válida?", "type": "main", "index": 0 } ] ] }, "¿Solicitud de acceso válida?": { "main": [ [ { "node": "Crear solicitud en Supabase", "type": "main", "index": 0 } ], [ { "node": "Responder solicitud inválida", "type": "main", "index": 0 } ] ] }, "Crear solicitud en Supabase": { "main": [ [ { "node": "Confirmar registro de solicitud", "type": "main", "index": 0 } ] ] }, "Preparar correos de aprobación": { "main": [ [ { "node": "¿Solicitud registrada?", "type": "main", "index": 0 } ] ] }, "¿Solicitud registrada?": { "main": [ [ { "node": "Enviar solicitud a aprobadores", "type": "main", "index": 0 } ], [ { "node": "Responder error al registrar", "type": "main", "index": 0 } ] ] }, "Enviar solicitud a aprobadores": { "main": [ [ { "node": "Responder solicitud enviada", "type": "main", "index": 0 } ] ] }, "Webhook - Confirmar decisión": { "main": [ [ { "node": "Construir página de confirmación", "type": "main", "index": 0 } ] ] }, "Construir página de confirmación": { "main": [ [ { "node": "Mostrar confirmación", "type": "main", "index": 0 } ] ] }, "Webhook - Resolver solicitud": { "main": [ [ { "node": "Validar decisión", "type": "main", "index": 0 } ] ] }, "Validar decisión": { "main": [ [ { "node": "¿Decisión válida?", "type": "main", "index": 0 } ] ] }, "¿Decisión válida?": { "main": [ [ { "node": "Registrar decisión en Supabase", "type": "main", "index": 0 } ], [ { "node": "Preparar decisión inválida", "type": "main", "index": 0 } ] ] }, "Preparar decisión inválida": { "main": [ [ { "node": "Responder decisión inválida", "type": "main", "index": 0 } ] ] }, "Registrar decisión en Supabase": { "main": [ [ { "node": "Preparar resultado de decisión", "type": "main", "index": 0 } ] ] }, "Preparar resultado de decisión": { "main": [ [ { "node": "¿Notificar a IT Support?", "type": "main", "index": 0 } ] ] }, "¿Notificar a IT Support?": { "main": [ [ { "node": "Enviar decisión a IT Support", "type": "main", "index": 0 } ], [ { "node": "Mostrar resultado final", "type": "main", "index": 0 } ] ] }, "Enviar decisión a IT Support": { "main": [ [ { "node": "Mostrar resultado final", "type": "main", "index": 0 } ] ] }, "Confirmar registro de solicitud": { "main": [ [ { "node": "¿Solicitud registrada en Supabase?", "type": "main", "index": 0 } ] ] }, "¿Solicitud registrada en Supabase?": { "main": [ [ { "node": "Emitir enlaces de aprobación", "type": "main", "index": 0 } ], [ { "node": "Responder error al registrar", "type": "main", "index": 0 } ] ] }, "Emitir enlaces de aprobación": { "main": [ [ { "node": "Preparar correos de aprobación", "type": "main", "index": 0 } ] ] } }, "authors": "Isaac Aracena", "name": "Version f0bf0a23", "description": "", "autosaved": true, "workflowPublishHistory": [ { "createdAt": "2026-08-15T13:32:14.699Z", "id": 4768, "workflowId": "RaNkDfE2YCrZgCbx", "versionId": "f0bf0a23-7940-4d51-b12c-00ae0fd7cc50", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" } ] } }