Files
adam-acs/app/services/references/providers/wassup_provider.ts
T
Kevin L'Hereec e857b4fbe7 feat: reference providers system with Wassup integration
- Pluggable multi-provider architecture (ReferenceEngine)
- Wassup provider implementation (HTTP GET + X_WASSUP_USER_IP)
- Auto-resolve references on Inform (BOOT/BOOTSTRAP events)
- References displayed in device Identity card
- REST API: GET/POST references per device
- Config via config/references.json
- Fix event extraction for single EventStruct and numeric EventCodes
- Mock Wassup provisioned by home-device-simulator on device creation
2026-06-02 15:52:16 +02:00

159 lines
4.8 KiB
TypeScript

import type { DeviceRecord } from '../../../domain/device/device_record.js'
import type {
ReferenceProvider,
ResolvedReference,
InformContext,
ProviderConfig,
} from '../types.js'
import { ReferenceEngine } from '../reference_engine.js'
/**
* Wassup Provider — resolves customer identity via the Wassup protocol.
*
* Protocol:
* - HTTP GET to {url}/wassup?info=sai,spr,...
* - Header X_WASSUP_USER_IP with the CPE IP address
* - Response: text/plain with key=value lines (X_WASSUP_STATUS=OK, X_WASSUP_SAI=..., etc.)
*/
export class WassupProvider implements ReferenceProvider {
name: string
enabled: boolean
private url: string
private timeout: number
private service: string
private requestedProperties: string[]
private resolveBy: string
constructor(config: ProviderConfig) {
this.name = config.name
this.enabled = config.enabled
this.url = config.url || ''
this.timeout = config.timeout || 3000
this.resolveBy = config.resolveBy || 'ip'
this.service = config.options?.service || 'KRMSCO'
this.requestedProperties = config.options?.requestedProperties || ['sai', 'spr', 'clo', 'puid', 'utz']
}
async resolve(device: DeviceRecord, context: InformContext): Promise<ResolvedReference | null> {
const resolveValue = ReferenceEngine.extractResolveValue(device, this.resolveBy, context)
if (!resolveValue) {
console.debug({ provider: this.name, resolveBy: this.resolveBy }, '[Wassup] Cannot resolve: no value')
return null
}
const infoParam = this.requestedProperties.join(',')
const wassupUrl = `${this.url}/wassup?wassup=ident&prot=200&charset=utf8&serv=${this.service}&info=${infoParam}`
try {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), this.timeout)
const response = await fetch(wassupUrl, {
method: 'GET',
headers: {
'X_WASSUP_USER_IP': resolveValue,
},
signal: controller.signal,
})
clearTimeout(timer)
if (!response.ok) {
return {
provider: this.name,
resolvedAt: new Date(),
resolvedBy: `${this.resolveBy}:${resolveValue}`,
status: 'error',
properties: {},
error: `HTTP ${response.status}`,
}
}
const body = await response.text()
const parsed = this.parseResponse(body)
const status = parsed['X_WASSUP_STATUS']
if (status === 'UNKNOWN_USER') {
return {
provider: this.name,
resolvedAt: new Date(),
resolvedBy: `${this.resolveBy}:${resolveValue}`,
status: 'unknown',
properties: {},
}
}
if (status !== 'OK') {
return {
provider: this.name,
resolvedAt: new Date(),
resolvedBy: `${this.resolveBy}:${resolveValue}`,
status: 'error',
properties: {},
error: parsed['X_WASSUP_ERROR'] || `Unexpected status: ${status}`,
}
}
// Map X_WASSUP_* keys to short names
const properties: Record<string, string> = {}
const keyMapping: Record<string, string> = {
'X_WASSUP_SAI': 'sai',
'X_WASSUP_SPR': 'spr',
'X_WASSUP_CLO': 'clo',
'X_WASSUP_PUID': 'puid',
'X_WASSUP_UTZ': 'utz',
'X_WASSUP_IUR': 'iur',
'X_WASSUP_IUS': 'ius',
}
for (const [wassupKey, shortKey] of Object.entries(keyMapping)) {
if (parsed[wassupKey] && parsed[wassupKey] !== '') {
properties[shortKey] = parsed[wassupKey]
}
}
console.info({ provider: this.name, resolveBy: `${this.resolveBy}:${resolveValue}`, keys: Object.keys(properties) }, '[Wassup] Resolved')
return {
provider: this.name,
resolvedAt: new Date(),
resolvedBy: `${this.resolveBy}:${resolveValue}`,
status: 'ok',
properties,
}
} catch (error: any) {
const isTimeout = error.name === 'AbortError'
console.error({ provider: this.name, error: error.message, isTimeout }, '[Wassup] Resolution failed')
return {
provider: this.name,
resolvedAt: new Date(),
resolvedBy: `${this.resolveBy}:${resolveValue}`,
status: 'error',
properties: {},
error: isTimeout ? 'Timeout' : error.message,
}
}
}
/**
* Parse a Wassup text/plain response into a key-value map.
* Format: "X_WASSUP_STATUS=OK\nX_WASSUP_SAI=111111111\n..."
*/
private parseResponse(body: string): Record<string, string> {
const result: Record<string, string> = {}
const lines = body.split('\n')
for (const line of lines) {
const idx = line.indexOf('=')
if (idx > 0) {
const key = line.slice(0, idx).trim()
const value = line.slice(idx + 1).trim()
result[key] = value
}
}
return result
}
}