Next gate
Gates are operated by District IT. Current role can review the package but not advance it.
src/Connector.php
sha256 7c1a0709cd5e093f06ff…1<?php 2declare(strict_types=1); 3 4namespace Msad17\Connectors\RaptorVisitor\B2610051209273; 5 6/** 7 * Raptor Visitor Management — events 8 * Connector build DX-261005-003 · v0.1.3 · generated 2026-10-05 12:09:27 EDT 9 * 10 * Generated by Integration Bot for the MSAD 17 Security Platform. 11 * Owner after promotion: District IT. Developer support after promotion: none. 12 * 13 * MODE: READ-ONLY. No write path (unlock, schedule, credential, PTZ) is compiled into 14 * this artifact. Write capability requires a separate, dual-approved package. 15 */ 16final class Connector 17{ 18 public const STACK = 'raptor_visitor'; 19 public const VERSION = '0.1.3'; 20 public const MODE = 'read_only'; 21 public const SCHEMA = 'msad17.event/1'; 22 23 /** Vendor event key => [normalized type, priority]. Source of truth: mapping/event-map.json */ 24 public const EVENT_MAP = [ 25 'VISITOR_SIGN_IN' => ['door.access_granted', 'info'], 26 'VISITOR_SIGN_OUT' => ['door.access_granted', 'info'], 27 'ALERT_WATCHLIST' => ['io.duress', 'p1'], 28 'EMERGENCY_LOCKDOWN' => ['io.duress', 'p1'], 29 'DENIED_ENTRY' => ['door.access_denied', 'p4'], 30 ]; 31 32 /** Dot-paths into the vendor payload. */ 33 public const PATHS = [ 34 'type' => 'eventType', 35 'time' => 'occurredAt', 36 'device' => 'buildingId', 37 'state' => null, 38 ]; 39 40 /** Endpoints this connector is allowed to call (enforced by sandbox + egress policy). */ 41 public const ENDPOINTS = [ 42 ['POST', '/api/oauth/token'], 43 ['GET', '/api/v1/visitors/active'], 44 ['GET', '/api/v1/events'], 45 ]; 46 47 /** @param array<string,mixed> $config validated against config/config.schema.json */ 48 public function __construct(private readonly Transport $transport, private readonly array $config) 49 { 50 foreach (['base_url', 'site_id'] as $k) { 51 if (empty($config[$k])) { 52 throw new \InvalidArgumentException("config.{$k} is required"); 53 } 54 } 55 } 56 57 /** Liveness + latency probe. Called by the platform health monitor every poll interval. */ 58 public function probe(): array 59 { 60 $t0 = microtime(true); 61 $res = $this->transport->request('GET', '/api/v1/visitors/active', null); 62 return [ 63 'ok' => $res['status'] >= 200 && $res['status'] < 300, 64 'status' => $res['status'], 65 'latency_ms' => (int)round((microtime(true) - $t0) * 1000), 66 'stack' => self::STACK, 67 'mode' => self::MODE, 68 ]; 69 } 70 71 /** 72 * Pull and normalize pending vendor events. 73 * @return array{events: list<array<string,mixed>>, unmapped: int, unmapped_keys: list<string>} 74 */ 75 public function pullEvents(): array 76 { 77 $res = $this->transport->request('GET', '/api/v1/events', null); 78 $out = []; 79 $unmapped = []; 80 foreach (($res['events'] ?? []) as $raw) { 81 $n = is_array($raw) ? $this->normalize($raw) : null; 82 if ($n === null) { 83 $unmapped[] = is_array($raw) ? (string)(self::get($raw, self::PATHS['type']) ?? '?') : '?'; 84 continue; 85 } 86 $out[] = $n; 87 } 88 return ['events' => $out, 'unmapped' => count($unmapped), 'unmapped_keys' => array_values(array_unique($unmapped))]; 89 } 90 91 /** Map one vendor payload to the platform event schema; null = not mapped (logged, never dropped silently). */ 92 public function normalize(array $raw): ?array 93 { 94 $key = self::get($raw, self::PATHS['type']); 95 if (!is_string($key) || !isset(self::EVENT_MAP[$key])) { 96 return null; 97 } 98 [$type, $priority] = self::EVENT_MAP[$key]; 99 $state = self::PATHS['state'] !== null ? self::get($raw, self::PATHS['state']) : null; 100 $rtn = is_scalar($state) && in_array(strtolower((string)$state), ['false', 'inactive', '0', 'normal'], true); 101 $vendorDevice = (string)(self::get($raw, self::PATHS['device']) ?? 'unknown'); 102 103 return [ 104 'schema' => self::SCHEMA, 105 'type' => $type, 106 'priority' => $priority, 107 'rtn' => $rtn, 108 'site_id' => (string)$this->config['site_id'], 109 'device_id' => (string)($this->config['device_map'][$vendorDevice] ?? $vendorDevice), 110 'vendor_device' => $vendorDevice, 111 'vendor_key' => $key, 112 'occurred_at' => self::utc(self::get($raw, self::PATHS['time'])), 113 'source' => self::STACK . '@' . (string)parse_url((string)$this->config['base_url'], PHP_URL_HOST), 114 'raw_sha256' => hash('sha256', (string)json_encode($raw)), 115 ]; 116 } 117 118 /** Credential-free live stream locator; the platform injects auth at session time. */ 119 public function liveUri(string $deviceId): ?string 120 { 121 $tpl = null; 122 return $tpl === null ? null : str_replace('{device}', rawurlencode($deviceId), $tpl); 123 } 124 125 private static function get(mixed $a, ?string $path): mixed 126 { 127 if ($path === null || $path === '') { 128 return null; 129 } 130 foreach (explode('.', $path) as $seg) { 131 if (!is_array($a) || !array_key_exists($seg, $a)) { 132 return null; 133 } 134 $a = $a[$seg]; 135 } 136 return $a; 137 } 138 139 private static function utc(mixed $v): string 140 { 141 if (is_int($v) || is_float($v) || (is_string($v) && ctype_digit($v))) { 142 $n = (int)$v; 143 return gmdate('Y-m-d\TH:i:s\Z', $n > 20000000000 ? intdiv($n, 1000) : $n); 144 } 145 if (is_string($v) && ($t = strtotime($v)) !== false) { 146 return gmdate('Y-m-d\TH:i:s\Z', $t); 147 } 148 return gmdate('Y-m-d\TH:i:s\Z'); 149 } 150}