{ "kind": "open-flow-workflow", "version": 1, "name": "Validate a TOTP Code", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_84c77d81a9bc3199": { "name": "Enter the code", "kind": "start", "inputDefs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "code" } ] }, "n_ee081add5ca48aa0": { "inputs": { "secret": { "kind": "unset" }, "code": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_84c77d81a9bc3199", "output": "code" } ] } }, "name": "Validate current TOTP", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "secret" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "code" } ], "outputs": [ { "jsonSchema": { "type": "integer", "enum": [ 0, 1 ] }, "nullable": false, "handle": "status" } ], "name": "Validate current TOTP", "moduleId": "n_77e6e8b9caaeeb8b", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_918338017350a09c": { "name": "Validation result", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "integer", "enum": [ 0, 1 ] }, "nullable": false, "handle": "status" } ], "outputMappings": { "status": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_ee081add5ca48aa0", "output": "status" } ] } } } }, "edges": [ { "source": "n_84c77d81a9bc3199", "target": "n_ee081add5ca48aa0" }, { "source": "n_ee081add5ca48aa0", "target": "n_918338017350a09c" } ] } }, "modules": { "n_77e6e8b9caaeeb8b": { "name": "Validate current TOTP", "imports": [], "source": "import{createHmac}from'node:crypto';import{Buffer}from'node:buffer';\nexport default function verify({secret,code}){const alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',s=secret.toUpperCase().replace(/=+$/,'');if(!s||!/^[A-Z2-7]+$/.test(s)||![0,2,4,5,7].includes(s.length%8))throw Error('Invalid Base32 secret');let bits=0,v=0,bytes=[];for(const c of s){v=(v<<5)|alphabet.indexOf(c);bits+=5;if(bits>=8){bits-=8;bytes.push((v>>>bits)&255)}}const counter=BigInt(Math.floor(Date.now()/30000)),buffer=Buffer.alloc(8);for(let i=0;i<8;i++)buffer[7-i]=Number((counter>>BigInt(i*8))&255n);const digest=createHmac('sha1',Buffer.from(bytes)).update(buffer).digest(),off=digest[digest.length-1]&15,n=(((digest[off]&127)<<24)|(digest[off+1]<<16)|(digest[off+2]<<8)|digest[off+3])%1000000;return{status:String(n).padStart(6,'0')===code?1:0}}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }