{ "name": "DHCP Option 82 Auth", "description": "WEBHOOK: Always upserts IP. If Option 82 present AND MAC linked to an account, syncs all inventoried MACs to Preseem with plan details.", "trigger": { "type": "WEBHOOK", "inputJsonSchema": { "type": "object", "required": ["bound", "mac", "ip", "server", "remoteId"], "properties": { "bound": { "type": "string" }, "mac": { "type": "string" }, "ip": { "type": "string" }, "server": { "type": "string" }, "remoteId": { "type": "string" } } }, "nextSteps": [{ "slug": "check-inventory" }] }, "steps": [ { "name": "Find inventory item by MAC", "slug": "check-inventory", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "allowSkipExecution": false, "outputMapper": "({ output, state }) => {\n const items = output.response.findInventoryItemsFromDeviceIdentifiers;\n // Accept items assigned to Accounts OR NetworkSites\n const assigned = items.filter(\n (item) => {\n const type = item.inventoryItem?.assignation?.assignee?.__typename;\n return type === 'Account' || type === 'NetworkSite';\n }\n );\n if (assigned.length === 0) return null;\n const inv = assigned[0].inventoryItem;\n const assignee = inv.assignation.assignee;\n return {\n assigneeId: assignee.id,\n assigneeType: assignee.__typename,\n assigneeName: assignee.name || ''\n };\n}", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query FindItem($input: FindInventoryItemsFromDeviceIdentifiersQueryInput!) {\n findInventoryItemsFromDeviceIdentifiers(input: $input) {\n identifier\n inventoryItem {\n id\n model { name }\n assignation {\n assignee {\n __typename\n ... on Account { id name }\n }\n }\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => {\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n return { input: { identifiers: [formattedMac] } };\n}" } ], "nextSteps": [{ "slug": "is-connect" }] }, { "name": "Connect or disconnect?", "slug": "is-connect", "type": "CONDITION", "nodeSlug": "condition", "condition": "({ secrets, state, utils, variables }) => {\n return state.input.bound === '1';\n}", "elseSteps": [{ "slug": "expire-ip" }], "thenSteps": [{ "slug": "has-account-connect" }] }, { "name": "Account found? (connect)", "slug": "has-account-connect", "type": "CONDITION", "nodeSlug": "condition", "condition": "({ secrets, state, utils, variables }) => {\n return state.steps['check-inventory'].output != null;\n}", "elseSteps": [{ "slug": "upsert-ip-no-account" }], "thenSteps": [{ "slug": "upsert-ip-with-account" }] }, { "name": "Upsert IP (no account)", "slug": "upsert-ip-no-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "allowSkipExecution": false, "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation UpsertIp($input: UpsertMacAddressIpAddressAssignationsInput!) {\n upsertMacAddressIpAddressAssignations(input: $input) {\n assignations { macAddress ipAddress }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => {\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n return {\n input: {\n assignations: { macAddress: formattedMac, ipAddress: state.input.ip, expired: false }\n }\n };\n}" } ], "nextSteps": [{ "slug": "end" }] }, { "name": "Upsert IP (with account)", "slug": "upsert-ip-with-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "allowSkipExecution": false, "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation UpsertIp($input: UpsertMacAddressIpAddressAssignationsInput!) {\n upsertMacAddressIpAddressAssignations(input: $input) {\n assignations { macAddress ipAddress }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => {\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n return {\n input: {\n assignations: { macAddress: formattedMac, ipAddress: state.input.ip, expired: false }\n }\n };\n}" } ], "nextSteps": [{ "slug": "should-sync-preseem" }] }, { "name": "Has Option 82 and account?", "slug": "should-sync-preseem", "type": "CONDITION", "nodeSlug": "condition", "condition": "({ secrets, state, utils, variables }) => {\n const inv = state.steps['check-inventory'].output;\n if (inv == null) return false;\n if (!state.input.remoteId || state.input.remoteId === '') return false;\n return inv.assigneeType === 'Account';\n}", "elseSteps": [{ "slug": "should-sync-site" }], "thenSteps": [{ "slug": "get-account-macs-and-plan" }] }, { "name": "Get all inventoried MACs and plan", "slug": "get-account-macs-and-plan", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "allowSkipExecution": false, "outputMapper": "({ output, state }) => {\n const account = output.response.account;\n if (!account) {\n workflow.fail('Account not found', 'ACCOUNT_NOT_FOUND');\n }\n const macs = [];\n for (const assign of (account.assignedInventoryItems?.nodes || [])) {\n const inv = assign.inventoryItem || {};\n for (const f of (inv.fields?.nodes || [])) {\n const fieldName = (f.modelField?.name || '').toLowerCase();\n const data = (f.data || '').trim();\n if (fieldName.includes('mac') && data && data !== '00:00:00:00:00:00') {\n const cleaned = data.startsWith('1:') ? data.slice(2) : data;\n const parts = cleaned.split(':');\n if (parts.length === 6) {\n macs.push(parts.map((p) => p.padStart(2, '0').toUpperCase()).join(':'));\n }\n }\n }\n for (const a of (inv.ipAddressAssignments?.nodes || [])) {\n if (a.macAddress) macs.push(a.macAddress.toUpperCase());\n }\n }\n const plan = (account.billingSubscriptions?.nodes || [])\n .map((s) => s.productVersion?.product?.slug)\n .find(Boolean) || '';\n return { macs: [...new Set(macs)], plan, gaiiaId: account.readableId };\n}", "inputs": [ { "key": "query", "type": "FUNCTION", "value": "({ state }) => {\n const accountId = state.steps['check-inventory'].output.accountId;\n return `query { account(id: \"${accountId}\") { readableId assignedInventoryItems { nodes { inventoryItem { fields { nodes { data modelField { name } } } ipAddressAssignments { nodes { macAddress } } } } } billingSubscriptions { nodes { productVersion { product { slug } } } } } }`;\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({})" } ], "nextSteps": [{ "slug": "preseem-update" }] }, { "name": "Update Preseem", "slug": "preseem-update", "type": "PRESEEM_REST_REQUEST", "nodeSlug": "preseem-rest-request", "allowSkipExecution": false, "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId } = state.steps['get-account-macs-and-plan'].output;\n return `/services/${gaiiaId}`;\n}" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId, macs, plan } = state.steps['get-account-macs-and-plan'].output;\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const connectingMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toLowerCase();\n const framedIp = state.input.ip;\n const inventoriedMacs = macs.map((m) => m.toLowerCase());\n const attachments = inventoriedMacs\n .filter((m) => state.input.bound === '0' || m !== connectingMac)\n .map((m) => ({ cpe_mac: m }));\n if (state.input.bound === '1') {\n attachments.push({ cpe_mac: connectingMac, network_prefixes: [framedIp] });\n }\n return {\n id: String(gaiiaId),\n attachments,\n account: String(gaiiaId),\n package: plan\n };\n}" } ], "nextSteps": [{ "slug": "end" }] }, { "name": "Expire IP", "slug": "expire-ip", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "allowSkipExecution": false, "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation ExpireIp($input: UpsertMacAddressIpAddressAssignationsInput!) {\n upsertMacAddressIpAddressAssignations(input: $input) {\n assignations { macAddress ipAddress }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => {\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const formattedMac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toUpperCase();\n return {\n input: {\n assignations: { macAddress: formattedMac, ipAddress: state.input.ip, expired: true }\n }\n };\n}" } ], "nextSteps": [{ "slug": "should-sync-preseem-disconnect" }] }, { "name": "Network site with Option 82?", "slug": "should-sync-site", "type": "CONDITION", "nodeSlug": "condition", "condition": "({ secrets, state, utils, variables }) => {\n const inv = state.steps['check-inventory'].output;\n if (inv == null) return false;\n if (!state.input.remoteId || state.input.remoteId === '') return false;\n return inv.assigneeType === 'NetworkSite';\n}", "elseSteps": [{ "slug": "end" }], "thenSteps": [{ "slug": "preseem-site-update" }] }, { "name": "Update Preseem (site)", "slug": "preseem-site-update", "type": "PRESEEM_REST_REQUEST", "nodeSlug": "preseem-rest-request", "allowSkipExecution": false, "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => {\n const inv = state.steps['check-inventory'].output;\n return `/access_points/${inv.assigneeId}`;\n}" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const inv = state.steps['check-inventory'].output;\n const rawMac = state.input.mac;\n const cleanedMac = rawMac.startsWith('1:') ? rawMac.slice(2) : rawMac;\n const mac = cleanedMac\n .split(':')\n .map((segment) => segment.padStart(2, '0'))\n .join(':')\n .toLowerCase();\n return {\n id: inv.assigneeId,\n name: inv.assigneeName || inv.assigneeId,\n tower: inv.assigneeName || '',\n ip_address: state.input.ip\n };\n}" } ], "nextSteps": [{ "slug": "end" }] }, { "name": "Has Option 82 and account? (disconnect)", "slug": "should-sync-preseem-disconnect", "type": "CONDITION", "nodeSlug": "condition", "condition": "({ secrets, state, utils, variables }) => {\n const inv = state.steps['check-inventory'].output;\n if (inv == null) return false;\n if (!state.input.remoteId || state.input.remoteId === '') return false;\n return inv.assigneeType === 'Account';\n}", "elseSteps": [{ "slug": "should-sync-site-disconnect" }], "thenSteps": [{ "slug": "get-account-macs-and-plan" }] }, { "name": "Network site? (disconnect)", "slug": "should-sync-site-disconnect", "type": "CONDITION", "nodeSlug": "condition", "condition": "({ secrets, state, utils, variables }) => {\n const inv = state.steps['check-inventory'].output;\n if (inv == null) return false;\n if (!state.input.remoteId || state.input.remoteId === '') return false;\n return inv.assigneeType === 'NetworkSite';\n}", "elseSteps": [{ "slug": "end" }], "thenSteps": [{ "slug": "preseem-site-update" }] } ] }