feat(15-01): add Callback Token Encoder and Decoder utility nodes
- Callback Token Encoder: compress 129-char Unraid PrefixedID to 8-char hex token - SHA-256 hashing with 7-window collision detection (56 chars / 8-char windows) - Callback Token Decoder: resolve 8-char token back to PrefixedID - Both use JSON serialization for static data persistence (_callbackTokens) - Standalone utility nodes at [600,2400] and [1000,2400] - Not connected - Phase 16 will wire into active flow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4956,6 +4956,34 @@
|
|||||||
2600
|
2600
|
||||||
],
|
],
|
||||||
"onError": "continueRegularOutput"
|
"onError": "continueRegularOutput"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"mode": "runOnceForAllItems",
|
||||||
|
"jsCode": "// Callback Token Encoder\n// Compresses 129-char Unraid PrefixedID to 8-char token for Telegram callback_data\n\n// Initialize token store using static data with JSON serialization\nconst staticData = $getWorkflowStaticData('global');\nif (!staticData._callbackTokens) {\n staticData._callbackTokens = JSON.stringify({});\n}\nconst tokenStore = JSON.parse(staticData._callbackTokens);\n\n/**\n * Encode Unraid PrefixedID to 8-character token\n * Uses SHA-256 with collision detection\n * @param {string} unraidId - 129-char PrefixedID (server_hash:container_hash)\n * @returns {string} 8-character hex token\n */\nasync function encodeToken(unraidId) {\n // Generate SHA-256 hash\n const encoder = new TextEncoder();\n const data = encoder.encode(unraidId);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n \n // Try up to 7 non-overlapping 8-char windows (56 chars of SHA-256)\n for (let offset = 0; offset < 56; offset += 8) {\n const token = hashHex.substring(offset, offset + 8);\n \n // Check for collision\n if (tokenStore[token]) {\n // If same unraidId, reuse token (idempotent)\n if (tokenStore[token] === unraidId) {\n return token;\n }\n // Collision with different ID - try next window\n continue;\n }\n \n // No collision - store and return\n tokenStore[token] = unraidId;\n staticData._callbackTokens = JSON.stringify(tokenStore);\n return token;\n }\n \n // All 7 windows collided (extremely unlikely)\n throw new Error(`Token collision: all 7 hash windows collided for ${unraidId.substring(0, 20)}...`);\n}\n\n// Process input\nconst input = $input.item.json;\nconst unraidId = input.unraidId;\n\nif (!unraidId) {\n throw new Error('Missing required input: unraidId');\n}\n\n// Encode the token\nconst token = await encodeToken(unraidId);\n\n// Build callback data if action provided\nlet callbackData = null;\nlet byteSize = null;\nlet warning = null;\n\nif (input.action) {\n callbackData = `action:${input.action}:${token}`;\n byteSize = new TextEncoder().encode(callbackData).length;\n \n if (byteSize > 64) {\n warning = `Callback data exceeds 64-byte limit: ${byteSize} bytes`;\n }\n}\n\nreturn {\n token,\n unraidId,\n callbackData,\n byteSize,\n warning\n};\n"
|
||||||
|
},
|
||||||
|
"id": "code-callback-token-encoder",
|
||||||
|
"name": "Callback Token Encoder",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
2400
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"mode": "runOnceForAllItems",
|
||||||
|
"jsCode": "// Callback Token Decoder\n// Resolves 8-char token back to 129-char Unraid PrefixedID\n\n// Load token store from static data\nconst staticData = $getWorkflowStaticData('global');\nconst tokenStore = JSON.parse(staticData._callbackTokens || '{}');\n\n/**\n * Decode token to Unraid PrefixedID\n * @param {string} token - 8-character hex token\n * @returns {string} Unraid PrefixedID (129-char)\n * @throws {Error} If token not found\n */\nfunction decodeToken(token) {\n const unraidId = tokenStore[token];\n \n if (!unraidId) {\n throw new Error(`Token not found: ${token}. Token store may have been cleared.`);\n }\n \n return unraidId;\n}\n\n// Process input\nconst input = $input.item.json;\nlet token = input.token;\nlet action = null;\n\n// If callbackData provided, parse it\nif (!token && input.callbackData) {\n const parts = input.callbackData.split(':');\n \n // Expected format: \"action:start:a1b2c3d4\" or \"action:stop:a1b2c3d4\"\n if (parts.length >= 3) {\n action = parts[1]; // e.g., \"start\", \"stop\"\n token = parts[parts.length - 1]; // Last segment is token\n } else {\n throw new Error(`Invalid callbackData format: ${input.callbackData}. Expected \"action:<action>:<token>\"`);\n }\n}\n\nif (!token) {\n throw new Error('Missing required input: token or callbackData');\n}\n\n// Decode the token\nconst unraidId = decodeToken(token);\n\nreturn {\n token,\n unraidId,\n action\n};\n"
|
||||||
|
},
|
||||||
|
"id": "code-callback-token-decoder",
|
||||||
|
"name": "Callback Token Decoder",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1000,
|
||||||
|
2400
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"connections": {
|
"connections": {
|
||||||
|
|||||||
Reference in New Issue
Block a user