39 lines
21 KiB
JSON
39 lines
21 KiB
JSON
{
|
|
"schema_version": 1,
|
|
"channel": "homelab-runtime",
|
|
"command_id": "SUPPORT-260918-GRAM-SUPPORT-STAFF-EMAIL-SOURCE-READONLY-516R1R4",
|
|
"status": "OK",
|
|
"rc": 0,
|
|
"host": "pve01",
|
|
"mode": "read-only",
|
|
"component": "gram-support-staff-email-source-readonly",
|
|
"started_at_utc": "2026-09-18T05:14:57Z",
|
|
"finished_at_utc": "2026-09-18T05:15:08Z",
|
|
"reference_register_checked": true,
|
|
"reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66",
|
|
"error_register_checked": true,
|
|
"error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0",
|
|
"command_sha256": "08ff6acd78dea5c4a5621dcca2fedad234c67817f25ff1f3b4ee9b9b2d14c5f0",
|
|
"duplicate_failed_command_blocked": false,
|
|
"block_reason": null,
|
|
"execution_started": true,
|
|
"change_declared": false,
|
|
"result_contract_valid": true,
|
|
"result_contract_status": null,
|
|
"result_contract_error": null,
|
|
"command_rc": 0,
|
|
"changes_made": false,
|
|
"rollback_started": false,
|
|
"rollback_restored": null,
|
|
"mutation_outcome": "NO_MUTATION",
|
|
"sanitized": true,
|
|
"secrets_included": false,
|
|
"private_addresses_included": false,
|
|
"raw_evidence_retained_locally": true,
|
|
"raw_evidence_sha256": "3375e95bfa30e7b6adf9c22d4a03b7407be9113f1d2ed450ec96bed61add1e9a",
|
|
"sanitized_output_sha256": "3375e95bfa30e7b6adf9c22d4a03b7407be9113f1d2ed450ec96bed61add1e9a",
|
|
"output_truncated_in_json": false,
|
|
"full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt",
|
|
"output": "GRAM516R1R4_SAFE_REPORT={\"classification\":{\"all_sites_eligible_staff_match_target\":true,\"changes_made\":false,\"eligible_staff_target_match_total\":7,\"eligible_staff_total\":7,\"email_consumer_present\":true,\"event_dispatcher_present\":true,\"runtime_probe_7of7\":true,\"send_attempts\":0,\"source_probe_ok\":true},\"schema_version\":1,\"scope\":\"READ_ONLY_GRAM_SUPPORT_STAFF_EMAIL_SOURCE\",\"secret_policy\":\"NO_NON_TARGET_EMAIL_ADDRESSES_EXPORTED\",\"sites\":[{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"codevipe\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"dsmods\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"gamevipe\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"hkmods\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"kingofwolk\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"zakrutim\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"newfi\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]}],\"source\":{\"files\":{\"Notification/DeliveryGate.php\":{\"line_count\":16,\"present\":true,\"text\":\"<?php\\n\\nnamespace Gram\\\\Support\\\\Notification;\\n\\ninterface DeliveryGate\\n{\\n /**\\n * Return true only when this exact event/consumer/recipient delivery\\n * has not already been completed and this caller owns the retry lease.\\n */\\n public function acquire(int $eventId, string $consumer, string $recipientKey): bool;\\n\\n public function markSent(int $eventId, string $consumer, string $recipientKey): void;\\n\\n public function markFailed(int $eventId, string $consumer, string $recipientKey, string $failureClass): void;\\n}\\n\"},\"Notification/Dispatcher.php\":{\"line_count\":67,\"present\":true,\"text\":\"<?php\\n\\nnamespace Gram\\\\Support\\\\Notification;\\n\\nuse Gram\\\\Support\\\\Entity\\\\SupportEvent;\\nuse Gram\\\\Support\\\\Entity\\\\Ticket;\\nuse XF\\\\Entity\\\\User;\\n\\nfinal class Dispatcher\\n{\\n private AlertConsumer $alerts;\\n private EmailConsumer $email;\\n\\n public function __construct(AlertConsumer $alerts, EmailConsumer $email)\\n {\\n $this->alerts = $alerts;\\n $this->email = $email;\\n }\\n\\n /**\\n * Owner-facing delivery. Staff alert fan-out is intentionally provided by\\n * dispatchStaff() so staff recipients must still pass Gram Support ACL.\\n */\\n public function dispatchOwner(SupportEvent $event, Ticket $ticket, ?User $owner): void\\n {\\n if (!Policy::mayEmitExternal($event))\\n {\\n return;\\n }\\n\\n if ($owner && $owner->user_id)\\n {\\n $this->alerts->consumeForUser($event, $ticket, $owner);\\n $this->email->consumeForRegistered($event, $ticket, $owner);\\n return;\\n }\\n\\n $this->email->consumeForGuest($event, $ticket);\\n }\\n\\n /**\\n * @param User[] $staffUsers\\n */\\n public function dispatchStaff(SupportEvent $event, Ticket $ticket, array $staffUsers): void\\n {\\n if (!Policy::mayEmitExternal($event))\\n {\\n return;\\n }\\n\\n foreach ($staffUsers as $staff)\\n {\\n if (!$staff instanceof User || !$staff->user_id)\\n {\\n continue;\\n }\\n\\n if (!$staff->hasPermission('gramSupport', 'viewAll')\\n && !$staff->hasPermission('gramSupport', 'manage'))\\n {\\n continue;\\n }\\n\\n $this->alerts->consumeForUser($event, $ticket, $staff);\\n }\\n }\\n}\\n\"},\"Notification/EmailConsumer.php\":{\"line_count\":107,\"present\":true,\"text\":\"<?php\\n\\nnamespace Gram\\\\Support\\\\Notification;\\n\\nuse Gram\\\\Support\\\\Entity\\\\SupportEvent;\\nuse Gram\\\\Support\\\\Entity\\\\Ticket;\\nuse XF\\\\Entity\\\\User;\\n\\nfinal class EmailConsumer\\n{\\n private DeliveryGate $gate;\\n\\n public function __construct(DeliveryGate $gate)\\n {\\n $this->gate = $gate;\\n }\\n\\n public function consumeForRegistered(SupportEvent $event, Ticket $ticket, User $recipient): bool\\n {\\n if (!Policy::mayEmitExternal($event) || !$recipient->user_id)\\n {\\n return false;\\n }\\n\\n if (!$ticket->user_id || (int)$ticket->user_id !== (int)$recipient->user_id)\\n {\\n return false;\\n }\\n\\n $recipientKey = RecipientKey::user((int)$recipient->user_id);\\n return $this->send(\\n $event,\\n $recipientKey,\\n function () use ($event, $ticket, $recipient)\\n {\\n \\\\XF::app()->mailer()->newMail()\\n ->setToUser($recipient)\\n ->setTemplate('gram_support_ticket_event', [\\n 'ticket' => $ticket,\\n 'event' => $event,\\n 'accessUrl' => null\\n ])\\n ->queue();\\n }\\n );\\n }\\n\\n public function consumeForGuest(SupportEvent $event, Ticket $ticket): bool\\n {\\n if (!Policy::mayEmitExternal($event))\\n {\\n return false;\\n }\\n\\n $email = trim((string)$ticket->guest_email);\\n if ($email === '')\\n {\\n return false;\\n }\\n\\n $recipientKey = RecipientKey::email($email);\\n\\n return $this->send(\\n $event,\\n $recipientKey,\\n function () use ($event, $ticket, $email)\\n {\\n // Raw guest access token is intentionally unavailable here:\\n // only its hash is persisted. Email links to the manual access\\n // page; the guest uses the one-time/saved access code.\\n \\\\XF::app()->mailer()->newMail()\\n ->setTo($email)\\n ->setTemplate('gram_support_ticket_event_guest', [\\n 'ticket' => $ticket,\\n 'event' => $event,\\n 'accessUrl' => \\\\XF::app()->router('public')->buildLink('canonical:support/access')\\n ])\\n ->queue();\\n }\\n );\\n }\\n\\n private function send(SupportEvent $event, string $recipientKey, callable $sender): bool\\n {\\n if (!$this->gate->acquire((int)$event->event_id, 'email', $recipientKey))\\n {\\n return false;\\n }\\n\\n try\\n {\\n $sender();\\n $this->gate->markSent((int)$event->event_id, 'email', $recipientKey);\\n return true;\\n }\\n catch (\\\\Throwable $e)\\n {\\n $this->gate->markFailed(\\n (int)$event->event_id,\\n 'email',\\n $recipientKey,\\n get_class($e)\\n );\\n throw $e;\\n }\\n }\\n}\\n\"},\"Notification/EntityDeliveryGate.php\":{\"line_count\":118,\"present\":true,\"text\":\"<?php\\n\\nnamespace Gram\\\\Support\\\\Notification;\\n\\nfinal class EntityDeliveryGate implements DeliveryGate\\n{\\n private const LEASE_SECONDS = 120;\\n\\n public function acquire(int $eventId, string $consumer, string $recipientKey): bool\\n {\\n if ($eventId < 1 || $consumer === '' || $recipientKey === '')\\n {\\n return false;\\n }\\n\\n $deliveryKey = $this->deliveryKey($eventId, $consumer, $recipientKey);\\n $lockName = 'gram_support_delivery_' . substr($deliveryKey, 0, 32);\\n $db = \\\\XF::db();\\n\\n if ((int)$db->fetchOne('SELECT GET_LOCK(?, 2)', [$lockName]) !== 1)\\n {\\n return false;\\n }\\n\\n try\\n {\\n $now = \\\\XF::$time;\\n $delivery = \\\\XF::em()->find('Gram\\\\Support:Delivery', $deliveryKey);\\n\\n if (!$delivery)\\n {\\n $delivery = \\\\XF::em()->create('Gram\\\\Support:Delivery');\\n $delivery->delivery_key = $deliveryKey;\\n $delivery->event_id = $eventId;\\n $delivery->consumer = $consumer;\\n $delivery->recipient_key = $recipientKey;\\n $delivery->state = 'pending';\\n $delivery->lease_expires = $now + self::LEASE_SECONDS;\\n $delivery->attempt_count = 1;\\n $delivery->created_date = $now;\\n $delivery->updated_date = $now;\\n $delivery->save();\\n return true;\\n }\\n\\n if ((string)$delivery->state === 'sent')\\n {\\n return false;\\n }\\n\\n if ((string)$delivery->state === 'pending' && (int)$delivery->lease_expires > $now)\\n {\\n return false;\\n }\\n\\n $delivery->state = 'pending';\\n $delivery->lease_expires = $now + self::LEASE_SECONDS;\\n $delivery->attempt_count = (int)$delivery->attempt_count + 1;\\n $delivery->updated_date = $now;\\n $delivery->save();\\n\\n return true;\\n }\\n finally\\n {\\n try\\n {\\n $db->fetchOne('SELECT RELEASE_LOCK(?)', [$lockName]);\\n }\\n catch (\\\\Throwable $ignored)\\n {\\n }\\n }\\n }\\n\\n public function markSent(int $eventId, string $consumer, string $recipientKey): void\\n {\\n $delivery = \\\\XF::em()->find(\\n 'Gram\\\\Support:Delivery',\\n $this->deliveryKey($eventId, $consumer, $recipientKey)\\n );\\n\\n if (!$delivery)\\n {\\n return;\\n }\\n\\n $delivery->state = 'sent';\\n $delivery->lease_expires = 0;\\n $delivery->updated_date = \\\\XF::$time;\\n $delivery->sent_date = \\\\XF::$time;\\n $delivery->save();\\n }\\n\\n public function markFailed(int $eventId, string $consumer, string $recipientKey, string $failureClass): void\\n {\\n $delivery = \\\\XF::em()->find(\\n 'Gram\\\\Support:Delivery',\\n $this->deliveryKey($eventId, $consumer, $recipientKey)\\n );\\n\\n if (!$delivery)\\n {\\n return;\\n }\\n\\n $delivery->state = 'failed';\\n $delivery->lease_expires = 0;\\n $delivery->last_failure_class = substr($failureClass, 0, 191);\\n $delivery->updated_date = \\\\XF::$time;\\n $delivery->save();\\n }\\n\\n private function deliveryKey(int $eventId, string $consumer, string $recipientKey): string\\n {\\n return hash('sha256', $eventId . \\\"\\\\0\\\" . $consumer . \\\"\\\\0\\\" . $recipientKey);\\n }\\n}\\n\"},\"Notification/EventDispatcher.php\":{\"line_count\":42,\"present\":true,\"text\":\"<?php\\n\\nnamespace Gram\\\\Support\\\\Notification;\\n\\nuse Gram\\\\Support\\\\Entity\\\\SupportEvent;\\nuse Gram\\\\Support\\\\Entity\\\\Ticket;\\n\\nfinal class EventDispatcher\\n{\\n public function dispatchSafely(SupportEvent $event, Ticket $ticket): void\\n {\\n try\\n {\\n $gate = new EntityDeliveryGate();\\n $alerts = new AlertConsumer($gate);\\n $email = new EmailConsumer($gate);\\n $dispatcher = new Dispatcher($alerts, $email);\\n\\n $owner = null;\\n if ((int)$ticket->user_id > 0)\\n {\\n $owner = \\\\XF::em()->find('XF:User', (int)$ticket->user_id);\\n }\\n\\n $dispatcher->dispatchOwner($event, $ticket, $owner);\\n\\n if ((int)$ticket->assigned_user_id > 0)\\n {\\n $staff = \\\\XF::em()->find('XF:User', (int)$ticket->assigned_user_id);\\n if ($staff)\\n {\\n $dispatcher->dispatchStaff($event, $ticket, [$staff]);\\n }\\n }\\n }\\n catch (\\\\Throwable $e)\\n {\\n // Event/Ticket/Message are already persisted truth.\\n \\\\XF::logException($e, false, 'Gram Support notification consumer: ');\\n }\\n }\\n}\\n\"},\"Notification/Policy.php\":{\"line_count\":61,\"present\":true,\"text\":\"<?php\\n\\nnamespace Gram\\\\Support\\\\Notification;\\n\\nuse Gram\\\\Support\\\\Entity\\\\SupportEvent;\\n\\nfinal class Policy\\n{\\n private const INTERNAL_EVENT_TYPES = [\\n 'internal_note_created',\\n 'internal_note_updated',\\n 'internal_note_deleted'\\n ];\\n\\n public static function mayEmitExternal(SupportEvent $event): bool\\n {\\n if (in_array((string)$event->event_type, self::INTERNAL_EVENT_TYPES, true))\\n {\\n return false;\\n }\\n\\n $payload = self::safePayload($event);\\n\\n if (!empty($payload['is_internal']))\\n {\\n return false;\\n }\\n\\n return true;\\n }\\n\\n public static function safePayload(SupportEvent $event): array\\n {\\n $payload = $event->safe_payload_json;\\n\\n if (is_array($payload))\\n {\\n return $payload;\\n }\\n\\n if (!is_string($payload) || $payload === '')\\n {\\n return [];\\n }\\n\\n $decoded = json_decode($payload, true);\\n return is_array($decoded) ? $decoded : [];\\n }\\n\\n public static function alertExtraData(SupportEvent $event): array\\n {\\n $payload = self::safePayload($event);\\n\\n return [\\n 'support_event_id' => (int)$event->event_id,\\n 'ticket_id' => (int)$event->ticket_id,\\n 'event_type' => (string)$event->event_type,\\n 'public_ref' => isset($payload['public_ref']) ? (string)$payload['public_ref'] : ''\\n ];\\n }\\n}\\n\"},\"Notification/RecipientKey.php\":{\"line_count\":28,\"present\":true,\"text\":\"<?php\\n\\nnamespace Gram\\\\Support\\\\Notification;\\n\\nfinal class RecipientKey\\n{\\n public static function user(int $userId): string\\n {\\n if ($userId < 1)\\n {\\n throw new \\\\InvalidArgumentException('User recipient ID must be positive.');\\n }\\n\\n return 'user:' . $userId;\\n }\\n\\n public static function email(string $email): string\\n {\\n $email = strtolower(trim($email));\\n if ($email === '')\\n {\\n throw new \\\\InvalidArgumentException('Email recipient must not be empty.');\\n }\\n\\n // Never persist raw guest email in the delivery key.\\n return 'email-sha256:' . hash('sha256', $email);\\n }\\n}\\n\"}},\"ok\":true},\"source_transport\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"},\"target_email\":\"aleisaev@yandex.ru\",\"transport_evidence\":[{\"slug\":\"codevipe\",\"t\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"dsmods\",\"t\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"gamevipe\",\"t\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"hkmods\",\"t\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"kingofwolk\",\"t\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"zakrutim\",\"t\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}},{\"slug\":\"newfi\",\"t\":{\"err_truncated\":false,\"guest_stderr_present\":false,\"inner_rc\":0,\"ok\":true,\"out_truncated\":false,\"transport_rc\":0,\"transport_stderr_bytes\":0,\"transport_stderr_sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"}}]}\nGRAM516R1R4_DECISION=PASS_STAFF_EMAIL_SOURCE_READONLY\nGRAM516R1R4_NEXT_GATE=BUILD_MINIMAL_STAFF_EMAIL_CANARY_PATCH\nHOMELAB_RESULT_CONTRACT={\"version\":1,\"command_id\":\"SUPPORT-260918-GRAM-SUPPORT-STAFF-EMAIL-SOURCE-READONLY-516R1R4\",\"status\":\"OK\",\"changes_made\":false,\"rollback_started\":false,\"rollback_restored\":null}\n"
|
|
}
|