// Builds n8n/W5-purchase-check.json — public footer purchase/expiry webhook. // Query: GET /dk-purchase-check?u=&s= // dkey = DK_HOST_KEY ($env.DK_HOST_KEY) — a per-instance shared key; the // signature prevents username enumeration + forged requests from off-domain. // Response (JSON, CORS: https://drive.digikedai.com): // { username, known, purchases: [{product, product_id, expires_at, status, display_state}] } // Run: node n8n/gen_w5.js const fs = require('fs'); const path = require('path'); const body = fs.readFileSync(path.join(__dirname, 'w5-body.js'), 'utf8'); const workflow = { name: 'W5 - Footer Purchase Check (Public)', nodes: [ { parameters: { httpMethod: 'GET', path: 'dk-purchase-check', responseMode: 'responseNode', options: {} }, id: 'trigger-webhook', name: 'Footer Purchase Check (Webhook)', type: 'n8n-nodes-base.webhook', typeVersion: 2, position: [0, 0], webhookId: 'dk-purchase-check' }, { parameters: { jsCode: "// Extract + validate query params; verify HMAC signature.\n// u = username, s = HMAC-SHA256(username, $env.DK_HOST_KEY) hex.\nconst crypto = require('crypto');\n\nfunction sigOf(u) {\n return crypto.createHmac('sha256', $env.DK_HOST_KEY).update(u).digest('hex');\n}\n\nconst q = $input.item.json.query ?? {};\nconst username = String(q.u || '').trim();\nconst supplied = String(q.s || '').toLowerCase();\n\n// username must be non-empty; signature must match (timing-safe-ish)\nconst expected = sigOf(username);\nconst ok = username.length > 0 && supplied.length === 64 &&\n crypto.timingSafeEqual(Buffer.from(supplied), Buffer.from(expected));\n\nif (!ok) {\n return { json: { status: 403, body: { error: 'forbidden', username: '' } } };\n}\n\nreturn { json: { username, _ok: true } };\n" }, id: 'extract-validate', name: 'Extract & Validate Query', type: 'n8n-nodes-base.code', typeVersion: 2, position: [220, 0] }, { parameters: { jsCode: "// load env (Code node reads $env reliably only here) and pass through\nconst prev = $input.item.json;\nreturn { json: {\n username: prev.username,\n NOCODB_URL: $env.NOCODB_URL,\n NOCODB_TOKEN: $env.NOCODB_TOKEN,\n NOCODB_TABLE_CUSTOMERS: $env.NOCODB_TABLE_CUSTOMERS,\n NOCODB_TABLE_CUSTOMERPRODUCTS: $env.NOCODB_TABLE_CUSTOMERPRODUCTS,\n} };\n" }, id: 'load-env', name: 'Load Env', type: 'n8n-nodes-base.code', typeVersion: 2, position: [440, 0] }, { parameters: { jsCode: body }, id: 'compute', name: 'Lookup Purchases', type: 'n8n-nodes-base.code', typeVersion: 2, position: [660, 0] }, { parameters: { jsCode: "// Wrap in CORS + always-200 JSON envelope. On error return empty purchases.\nconst d = $input.item.json;\nreturn { json: {\n body: {\n username: d.username ?? '',\n known: d.known ?? false,\n purchases: d.purchases ?? [],\n }\n} };\n" }, id: 'respond', name: 'Respond JSON', type: 'n8n-nodes-base.code', typeVersion: 2, position: [880, 0] } ], connections: { 'Footer Purchase Check (Webhook)': { main: [[{ node: 'Extract & Validate Query', type: 'main', index: 0 }]] }, 'Extract & Validate Query': { main: [[{ node: 'Load Env', type: 'main', index: 0 }]] }, 'Load Env': { main: [[{ node: 'Lookup Purchases', type: 'main', index: 0 }]] }, 'Lookup Purchases': { main: [[{ node: 'Respond JSON', type: 'main', index: 0 }]] }, 'Respond JSON': { main: [] } }, settings: { executionOrder: 'v1' }, pinData: {} }; fs.writeFileSync(path.join(__dirname, 'W5-purchase-check.json'), JSON.stringify(workflow, null, 2) + '\n'); console.log('wrote W5-purchase-check.json (' + body.length + ' chars body)');