- 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
159 lines
4.8 KiB
TypeScript
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
|
|
}
|
|
}
|