feat(sanctions): add OFAC sanctions pressure intelligence (#1739)

* feat(sanctions): add OFAC sanctions pressure intelligence

* fix(sanctions): strip _state from API response, fix code/name alignment, cap seed limit

- trimResponse now destructures _state before spreading to prevent seed
  internals leaking to API clients during the atomicPublish→afterPublish window
- buildLocationMap and extractPartyCountries now sort (code, name) as aligned
  pairs instead of calling uniqueSorted independently on each array; fixes
  code↔name mispairing for OFAC-specific codes like XC (Crimea) where
  alphabetic order of codes and names diverges
- DEFAULT_RECENT_LIMIT reduced from 120 to 60 to match MAX_ITEMS_LIMIT so
  seeded entries beyond the handler cap are not written unnecessarily
- Add tests/sanctions-pressure.test.mjs covering all three invariants

* fix(sanctions): register sanctions:pressure:v1 in health.js BOOTSTRAP_KEYS and SEED_META

Adds sanctionsPressure to health.js so the health endpoint monitors the
seeded key for emptiness (CRIT) and freshness via seed-meta:sanctions:pressure
(maxStaleMin: 720 matches 12h seed TTL). Without this, health was blind to
stale or missing sanctions data.
This commit is contained in:
Elie Habib
2026-03-17 11:52:32 +04:00
committed by GitHub
parent a4e9e5e607
commit babb9b6836
36 changed files with 2242 additions and 10 deletions

2
api/bootstrap.js vendored
View File

@@ -50,6 +50,7 @@ const BOOTSTRAP_CACHE_KEYS = {
forecasts: 'forecast:predictions:v2', forecasts: 'forecast:predictions:v2',
securityAdvisories: 'intelligence:advisories-bootstrap:v1', securityAdvisories: 'intelligence:advisories-bootstrap:v1',
customsRevenue: 'trade:customs-revenue:v1', customsRevenue: 'trade:customs-revenue:v1',
sanctionsPressure: 'sanctions:pressure:v1',
}; };
const SLOW_KEYS = new Set([ const SLOW_KEYS = new Set([
@@ -62,6 +63,7 @@ const SLOW_KEYS = new Set([
'techEvents', 'techEvents',
'securityAdvisories', 'securityAdvisories',
'customsRevenue', 'customsRevenue',
'sanctionsPressure',
]); ]);
const FAST_KEYS = new Set([ const FAST_KEYS = new Set([
'earthquakes', 'outages', 'serviceStatuses', 'macroSignals', 'chokepoints', 'chokepointTransits', 'earthquakes', 'outages', 'serviceStatuses', 'macroSignals', 'chokepoints', 'chokepointTransits',

View File

@@ -35,6 +35,7 @@ const BOOTSTRAP_KEYS = {
forecasts: 'forecast:predictions:v2', forecasts: 'forecast:predictions:v2',
securityAdvisories: 'intelligence:advisories-bootstrap:v1', securityAdvisories: 'intelligence:advisories-bootstrap:v1',
customsRevenue: 'trade:customs-revenue:v1', customsRevenue: 'trade:customs-revenue:v1',
sanctionsPressure: 'sanctions:pressure:v1',
radiationWatch: 'radiation:observations:v1', radiationWatch: 'radiation:observations:v1',
}; };
@@ -130,6 +131,7 @@ const SEED_META = {
usniFleet: { key: 'seed-meta:military:usni-fleet', maxStaleMin: 420 }, usniFleet: { key: 'seed-meta:military:usni-fleet', maxStaleMin: 420 },
securityAdvisories: { key: 'seed-meta:intelligence:advisories', maxStaleMin: 90 }, securityAdvisories: { key: 'seed-meta:intelligence:advisories', maxStaleMin: 90 },
customsRevenue: { key: 'seed-meta:trade:customs-revenue', maxStaleMin: 1440 }, customsRevenue: { key: 'seed-meta:trade:customs-revenue', maxStaleMin: 1440 },
sanctionsPressure: { key: 'seed-meta:sanctions:pressure', maxStaleMin: 720 },
radiationWatch: { key: 'seed-meta:radiation:observations', maxStaleMin: 30 }, radiationWatch: { key: 'seed-meta:radiation:observations', maxStaleMin: 30 },
}; };

View File

@@ -0,0 +1,9 @@
export const config = { runtime: 'edge' };
import { createDomainGateway, serverOptions } from '../../../server/gateway';
import { createSanctionsServiceRoutes } from '../../../src/generated/server/worldmonitor/sanctions/v1/service_server';
import { sanctionsHandler } from '../../../server/worldmonitor/sanctions/v1/handler';
export default createDomainGateway(
createSanctionsServiceRoutes(sanctionsHandler, serverOptions),
);

View File

@@ -0,0 +1 @@
{"components":{"schemas":{"CountrySanctionsPressure":{"description":"CountrySanctionsPressure summarizes designation volume and recent additions by country.","properties":{"aircraftCount":{"format":"int32","type":"integer"},"countryCode":{"type":"string"},"countryName":{"type":"string"},"entryCount":{"format":"int32","type":"integer"},"newEntryCount":{"format":"int32","type":"integer"},"vesselCount":{"format":"int32","type":"integer"}},"type":"object"},"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"ListSanctionsPressureRequest":{"description":"ListSanctionsPressureRequest retrieves recent OFAC sanctions pressure state.","properties":{"maxItems":{"format":"int32","type":"integer"}},"type":"object"},"ListSanctionsPressureResponse":{"description":"ListSanctionsPressureResponse contains normalized OFAC pressure summaries and recent entries.","properties":{"aircraftCount":{"format":"int32","type":"integer"},"consolidatedCount":{"format":"int32","type":"integer"},"countries":{"items":{"$ref":"#/components/schemas/CountrySanctionsPressure"},"type":"array"},"datasetDate":{"format":"int64","type":"string"},"entries":{"items":{"$ref":"#/components/schemas/SanctionsEntry"},"type":"array"},"fetchedAt":{"format":"int64","type":"string"},"newEntryCount":{"format":"int32","type":"integer"},"programs":{"items":{"$ref":"#/components/schemas/ProgramSanctionsPressure"},"type":"array"},"sdnCount":{"format":"int32","type":"integer"},"totalCount":{"format":"int32","type":"integer"},"vesselCount":{"format":"int32","type":"integer"}},"type":"object"},"ProgramSanctionsPressure":{"description":"ProgramSanctionsPressure summarizes designation volume and recent additions by OFAC program.","properties":{"entryCount":{"format":"int32","type":"integer"},"newEntryCount":{"format":"int32","type":"integer"},"program":{"type":"string"}},"type":"object"},"SanctionsEntry":{"description":"SanctionsEntry is a normalized OFAC sanctions designation.","properties":{"countryCodes":{"items":{"type":"string"},"type":"array"},"countryNames":{"items":{"type":"string"},"type":"array"},"effectiveAt":{"format":"int64","type":"string"},"entityType":{"description":"SanctionsEntityType classifies the designated party.","enum":["SANCTIONS_ENTITY_TYPE_UNSPECIFIED","SANCTIONS_ENTITY_TYPE_ENTITY","SANCTIONS_ENTITY_TYPE_INDIVIDUAL","SANCTIONS_ENTITY_TYPE_VESSEL","SANCTIONS_ENTITY_TYPE_AIRCRAFT"],"type":"string"},"id":{"type":"string"},"isNew":{"type":"boolean"},"name":{"type":"string"},"note":{"type":"string"},"programs":{"items":{"type":"string"},"type":"array"},"sourceLists":{"items":{"type":"string"},"type":"array"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"SanctionsService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/sanctions/v1/list-sanctions-pressure":{"get":{"description":"ListSanctionsPressure retrieves normalized OFAC designation summaries and recent additions.","operationId":"ListSanctionsPressure","parameters":[{"in":"query","name":"max_items","required":false,"schema":{"format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSanctionsPressureResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"ListSanctionsPressure","tags":["SanctionsService"]}}}}

View File

@@ -0,0 +1,190 @@
openapi: 3.1.0
info:
title: SanctionsService API
version: 1.0.0
paths:
/api/sanctions/v1/list-sanctions-pressure:
get:
tags:
- SanctionsService
summary: ListSanctionsPressure
description: ListSanctionsPressure retrieves normalized OFAC designation summaries and recent additions.
operationId: ListSanctionsPressure
parameters:
- name: max_items
in: query
required: false
schema:
type: integer
format: int32
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ListSanctionsPressureResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
properties:
message:
type: string
description: Error message (e.g., 'user not found', 'database connection failed')
description: Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.
FieldViolation:
type: object
properties:
field:
type: string
description: The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')
description:
type: string
description: Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')
required:
- field
- description
description: FieldViolation describes a single validation error for a specific field.
ValidationError:
type: object
properties:
violations:
type: array
items:
$ref: '#/components/schemas/FieldViolation'
description: List of validation violations
required:
- violations
description: ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.
ListSanctionsPressureRequest:
type: object
properties:
maxItems:
type: integer
format: int32
description: ListSanctionsPressureRequest retrieves recent OFAC sanctions pressure state.
ListSanctionsPressureResponse:
type: object
properties:
entries:
type: array
items:
$ref: '#/components/schemas/SanctionsEntry'
countries:
type: array
items:
$ref: '#/components/schemas/CountrySanctionsPressure'
programs:
type: array
items:
$ref: '#/components/schemas/ProgramSanctionsPressure'
fetchedAt:
type: string
format: int64
datasetDate:
type: string
format: int64
totalCount:
type: integer
format: int32
sdnCount:
type: integer
format: int32
consolidatedCount:
type: integer
format: int32
newEntryCount:
type: integer
format: int32
vesselCount:
type: integer
format: int32
aircraftCount:
type: integer
format: int32
description: ListSanctionsPressureResponse contains normalized OFAC pressure summaries and recent entries.
SanctionsEntry:
type: object
properties:
id:
type: string
name:
type: string
entityType:
type: string
enum:
- SANCTIONS_ENTITY_TYPE_UNSPECIFIED
- SANCTIONS_ENTITY_TYPE_ENTITY
- SANCTIONS_ENTITY_TYPE_INDIVIDUAL
- SANCTIONS_ENTITY_TYPE_VESSEL
- SANCTIONS_ENTITY_TYPE_AIRCRAFT
description: SanctionsEntityType classifies the designated party.
countryCodes:
type: array
items:
type: string
countryNames:
type: array
items:
type: string
programs:
type: array
items:
type: string
sourceLists:
type: array
items:
type: string
effectiveAt:
type: string
format: int64
isNew:
type: boolean
note:
type: string
description: SanctionsEntry is a normalized OFAC sanctions designation.
CountrySanctionsPressure:
type: object
properties:
countryCode:
type: string
countryName:
type: string
entryCount:
type: integer
format: int32
newEntryCount:
type: integer
format: int32
vesselCount:
type: integer
format: int32
aircraftCount:
type: integer
format: int32
description: CountrySanctionsPressure summarizes designation volume and recent additions by country.
ProgramSanctionsPressure:
type: object
properties:
program:
type: string
entryCount:
type: integer
format: int32
newEntryCount:
type: integer
format: int32
description: ProgramSanctionsPressure summarizes designation volume and recent additions by OFAC program.

View File

@@ -0,0 +1,13 @@
syntax = "proto3";
package worldmonitor.sanctions.v1;
// CountrySanctionsPressure summarizes designation volume and recent additions by country.
message CountrySanctionsPressure {
string country_code = 1;
string country_name = 2;
int32 entry_count = 3;
int32 new_entry_count = 4;
int32 vessel_count = 5;
int32 aircraft_count = 6;
}

View File

@@ -0,0 +1,28 @@
syntax = "proto3";
package worldmonitor.sanctions.v1;
import "sebuf/http/annotations.proto";
import "worldmonitor/sanctions/v1/country_sanctions_pressure.proto";
import "worldmonitor/sanctions/v1/program_sanctions_pressure.proto";
import "worldmonitor/sanctions/v1/sanctions_entry.proto";
// ListSanctionsPressureRequest retrieves recent OFAC sanctions pressure state.
message ListSanctionsPressureRequest {
int32 max_items = 1 [(sebuf.http.query) = { name: "max_items" }];
}
// ListSanctionsPressureResponse contains normalized OFAC pressure summaries and recent entries.
message ListSanctionsPressureResponse {
repeated SanctionsEntry entries = 1;
repeated CountrySanctionsPressure countries = 2;
repeated ProgramSanctionsPressure programs = 3;
int64 fetched_at = 4;
int64 dataset_date = 5;
int32 total_count = 6;
int32 sdn_count = 7;
int32 consolidated_count = 8;
int32 new_entry_count = 9;
int32 vessel_count = 10;
int32 aircraft_count = 11;
}

View File

@@ -0,0 +1,10 @@
syntax = "proto3";
package worldmonitor.sanctions.v1;
// ProgramSanctionsPressure summarizes designation volume and recent additions by OFAC program.
message ProgramSanctionsPressure {
string program = 1;
int32 entry_count = 2;
int32 new_entry_count = 3;
}

View File

@@ -0,0 +1,26 @@
syntax = "proto3";
package worldmonitor.sanctions.v1;
// SanctionsEntityType classifies the designated party.
enum SanctionsEntityType {
SANCTIONS_ENTITY_TYPE_UNSPECIFIED = 0;
SANCTIONS_ENTITY_TYPE_ENTITY = 1;
SANCTIONS_ENTITY_TYPE_INDIVIDUAL = 2;
SANCTIONS_ENTITY_TYPE_VESSEL = 3;
SANCTIONS_ENTITY_TYPE_AIRCRAFT = 4;
}
// SanctionsEntry is a normalized OFAC sanctions designation.
message SanctionsEntry {
string id = 1;
string name = 2;
SanctionsEntityType entity_type = 3;
repeated string country_codes = 4;
repeated string country_names = 5;
repeated string programs = 6;
repeated string source_lists = 7;
int64 effective_at = 8;
bool is_new = 9;
string note = 10;
}

View File

@@ -0,0 +1,16 @@
syntax = "proto3";
package worldmonitor.sanctions.v1;
import "sebuf/http/annotations.proto";
import "worldmonitor/sanctions/v1/list_sanctions_pressure.proto";
// SanctionsService provides structured OFAC sanctions pressure data.
service SanctionsService {
option (sebuf.http.service_config) = {base_path: "/api/sanctions/v1"};
// ListSanctionsPressure retrieves normalized OFAC designation summaries and recent additions.
rpc ListSanctionsPressure(ListSanctionsPressureRequest) returns (ListSanctionsPressureResponse) {
option (sebuf.http.config) = {path: "/list-sanctions-pressure", method: HTTP_METHOD_GET};
}
}

View File

@@ -0,0 +1,377 @@
#!/usr/bin/env node
import { XMLParser } from 'fast-xml-parser';
import { CHROME_UA, loadEnvFile, runSeed, verifySeedKey } from './_seed-utils.mjs';
loadEnvFile(import.meta.url);
const CANONICAL_KEY = 'sanctions:pressure:v1';
const STATE_KEY = 'sanctions:pressure:state:v1';
const CACHE_TTL = 12 * 60 * 60;
const DEFAULT_RECENT_LIMIT = 60;
const OFAC_TIMEOUT_MS = 45_000;
const PROGRAM_CODE_RE = /^[A-Z0-9][A-Z0-9-]{1,24}$/;
const OFAC_SOURCES = [
{ label: 'SDN', url: 'https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/sdn_advanced.xml' },
{ label: 'CONSOLIDATED', url: 'https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/cons_advanced.xml' },
];
const XML_PARSER = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
removeNSPrefix: true,
parseTagValue: false,
trimValues: true,
});
function listify(value) {
if (Array.isArray(value)) return value;
return value == null ? [] : [value];
}
function textValue(value) {
if (value == null) return '';
if (typeof value === 'string') return value.trim();
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
if (typeof value === 'object') {
if (typeof value['#text'] === 'string') return value['#text'].trim();
if (typeof value.NamePartValue === 'string') return value.NamePartValue.trim();
}
return '';
}
function buildEpoch(parts) {
const year = Number(parts?.Year || 0);
if (!year) return 0;
const month = Math.max(1, Number(parts?.Month || 1));
const day = Math.max(1, Number(parts?.Day || 1));
return Date.UTC(year, month - 1, day);
}
function uniqueSorted(values) {
return [...new Set(values.filter(Boolean).map((value) => String(value).trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
}
function compactNote(value) {
const note = String(value || '').replace(/\s+/g, ' ').trim();
if (!note) return '';
return note.length > 240 ? `${note.slice(0, 237)}...` : note;
}
function extractDocumentedName(documentedName) {
const parts = listify(documentedName?.DocumentedNamePart)
.map((part) => textValue(part?.NamePartValue))
.filter(Boolean);
if (parts.length > 0) return parts.join(' ');
return textValue(documentedName);
}
function normalizeDateOfIssue(value) {
const epoch = buildEpoch(value);
return Number.isFinite(epoch) ? epoch : 0;
}
function buildReferenceMaps(doc) {
const refs = doc?.ReferenceValueSets ?? {};
const areaCodes = new Map();
for (const area of listify(refs?.AreaCodeValues?.AreaCode)) {
areaCodes.set(String(area.ID || ''), {
code: textValue(area),
name: String(area.Description || '').trim(),
});
}
const featureTypes = new Map();
for (const feature of listify(refs?.FeatureTypeValues?.FeatureType)) {
featureTypes.set(String(feature.ID || ''), textValue(feature));
}
const legalBasis = new Map();
for (const basis of listify(refs?.LegalBasisValues?.LegalBasis)) {
legalBasis.set(String(basis.ID || ''), String(basis.LegalBasisShortRef || textValue(basis) || '').trim());
}
return { areaCodes, featureTypes, legalBasis };
}
function buildLocationMap(doc, areaCodes) {
const locations = new Map();
for (const location of listify(doc?.Locations?.Location)) {
const ids = listify(location?.LocationAreaCode).map((item) => String(item.AreaCodeID || ''));
const mapped = ids.map((id) => areaCodes.get(id)).filter(Boolean);
// Sort code/name as pairs so codes[i] always corresponds to names[i]
const pairs = [...new Map(mapped.map((item) => [item.code, item.name])).entries()]
.filter(([code]) => code.length > 0)
.sort(([a], [b]) => a.localeCompare(b));
locations.set(String(location.ID || ''), {
codes: pairs.map(([code]) => code),
names: pairs.map(([, name]) => name),
});
}
return locations;
}
function extractPartyName(profile) {
const identities = listify(profile?.Identity);
const aliases = identities.flatMap((identity) => listify(identity?.Alias));
const primaryAlias = aliases.find((alias) => alias?.Primary === 'true')
|| aliases.find((alias) => alias?.AliasTypeID === '1403')
|| aliases[0];
return extractDocumentedName(primaryAlias?.DocumentedName);
}
function resolveEntityType(profile, featureTypes) {
const subtype = String(profile?.PartySubTypeID || '');
if (subtype === '1') return 'SANCTIONS_ENTITY_TYPE_VESSEL';
if (subtype === '2') return 'SANCTIONS_ENTITY_TYPE_AIRCRAFT';
const featureNames = listify(profile?.Feature)
.map((feature) => featureTypes.get(String(feature?.FeatureTypeID || '')) || '')
.filter(Boolean);
if (featureNames.some((name) => /birth|citizenship|nationality/i.test(name))) {
return 'SANCTIONS_ENTITY_TYPE_INDIVIDUAL';
}
return 'SANCTIONS_ENTITY_TYPE_ENTITY';
}
function extractPartyCountries(profile, featureTypes, locations) {
// Use a Map to deduplicate by code while preserving code→name alignment
const seen = new Map();
for (const feature of listify(profile?.Feature)) {
const featureType = featureTypes.get(String(feature?.FeatureTypeID || '')) || '';
if (!/location/i.test(featureType)) continue;
const versions = listify(feature?.FeatureVersion);
for (const version of versions) {
const locationIds = listify(version?.VersionLocation).map((item) => String(item?.LocationID || ''));
for (const locationId of locationIds) {
const location = locations.get(locationId);
if (!location) continue;
location.codes.forEach((code, i) => {
if (code && !seen.has(code)) seen.set(code, location.names[i] ?? '');
});
}
}
}
const sorted = [...seen.entries()].sort(([a], [b]) => a.localeCompare(b));
return {
countryCodes: sorted.map(([c]) => c),
countryNames: sorted.map(([, n]) => n),
};
}
function buildPartyMap(doc, featureTypes, locations) {
const parties = new Map();
for (const distinctParty of listify(doc?.DistinctParties?.DistinctParty)) {
const profile = distinctParty?.Profile;
const profileId = String(profile?.ID || distinctParty?.FixedRef || '');
if (!profileId) continue;
parties.set(profileId, {
name: extractPartyName(profile),
entityType: resolveEntityType(profile, featureTypes),
...extractPartyCountries(profile, featureTypes, locations),
});
}
return parties;
}
function extractPrograms(entry) {
const directPrograms = listify(entry?.SanctionsMeasure)
.map((measure) => textValue(measure?.Comment))
.filter((value) => PROGRAM_CODE_RE.test(value));
return uniqueSorted(directPrograms);
}
function extractEffectiveAt(entry) {
const dates = [];
for (const event of listify(entry?.EntryEvent)) {
const epoch = buildEpoch(event?.Date);
if (epoch > 0) dates.push(epoch);
}
for (const measure of listify(entry?.SanctionsMeasure)) {
const epoch = buildEpoch(measure?.DatePeriod?.Start?.From || measure?.DatePeriod?.Start);
if (epoch > 0) dates.push(epoch);
}
return dates.length > 0 ? Math.max(...dates) : 0;
}
function extractNote(entry, legalBasis) {
const comments = listify(entry?.SanctionsMeasure)
.map((measure) => textValue(measure?.Comment))
.filter((value) => value && !PROGRAM_CODE_RE.test(value));
if (comments.length > 0) return compactNote(comments[0]);
const legal = listify(entry?.EntryEvent)
.map((event) => legalBasis.get(String(event?.LegalBasisID || '')) || '')
.filter(Boolean);
return compactNote(legal[0] || '');
}
function buildEntriesForDocument(doc, sourceLabel) {
const { areaCodes, featureTypes, legalBasis } = buildReferenceMaps(doc);
const locations = buildLocationMap(doc, areaCodes);
const parties = buildPartyMap(doc, featureTypes, locations);
const datasetDate = normalizeDateOfIssue(doc?.DateOfIssue);
const entries = [];
for (const entry of listify(doc?.SanctionsEntries?.SanctionsEntry)) {
const profileId = String(entry?.ProfileID || '');
const party = parties.get(profileId);
const name = party?.name || 'Unnamed designation';
const programs = extractPrograms(entry);
entries.push({
id: `${sourceLabel}:${String(entry?.ID || profileId || name)}`,
name,
entityType: party?.entityType || 'SANCTIONS_ENTITY_TYPE_ENTITY',
countryCodes: party?.countryCodes ?? [],
countryNames: party?.countryNames ?? [],
programs: programs.length > 0 ? programs : [sourceLabel],
sourceLists: [sourceLabel],
effectiveAt: String(extractEffectiveAt(entry)),
isNew: false,
note: extractNote(entry, legalBasis),
});
}
return { entries, datasetDate };
}
function sortEntries(a, b) {
return (Number(b.isNew) - Number(a.isNew))
|| (Number(b.effectiveAt) - Number(a.effectiveAt))
|| a.name.localeCompare(b.name);
}
function buildCountryPressure(entries) {
const map = new Map();
for (const entry of entries) {
const codes = entry.countryCodes.length > 0 ? entry.countryCodes : ['XX'];
const names = entry.countryNames.length > 0 ? entry.countryNames : ['Unknown'];
codes.forEach((code, index) => {
const key = `${code}:${names[index] || names[0] || 'Unknown'}`;
const current = map.get(key) || {
countryCode: code,
countryName: names[index] || names[0] || 'Unknown',
entryCount: 0,
newEntryCount: 0,
vesselCount: 0,
aircraftCount: 0,
};
current.entryCount += 1;
if (entry.isNew) current.newEntryCount += 1;
if (entry.entityType === 'SANCTIONS_ENTITY_TYPE_VESSEL') current.vesselCount += 1;
if (entry.entityType === 'SANCTIONS_ENTITY_TYPE_AIRCRAFT') current.aircraftCount += 1;
map.set(key, current);
});
}
return [...map.values()]
.sort((a, b) => b.newEntryCount - a.newEntryCount || b.entryCount - a.entryCount || a.countryName.localeCompare(b.countryName))
.slice(0, 12);
}
function buildProgramPressure(entries) {
const map = new Map();
for (const entry of entries) {
const programs = entry.programs.length > 0 ? entry.programs : ['UNSPECIFIED'];
for (const program of programs) {
const current = map.get(program) || { program, entryCount: 0, newEntryCount: 0 };
current.entryCount += 1;
if (entry.isNew) current.newEntryCount += 1;
map.set(program, current);
}
}
return [...map.values()]
.sort((a, b) => b.newEntryCount - a.newEntryCount || b.entryCount - a.entryCount || a.program.localeCompare(b.program))
.slice(0, 12);
}
async function fetchSource(source) {
const response = await fetch(source.url, {
headers: { 'User-Agent': CHROME_UA },
signal: AbortSignal.timeout(OFAC_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`OFAC ${source.label} HTTP ${response.status}`);
}
const xml = await response.text();
const parsed = XML_PARSER.parse(xml)?.Sanctions;
if (!parsed) throw new Error(`OFAC ${source.label} parse returned no Sanctions root`);
return buildEntriesForDocument(parsed, source.label);
}
async function fetchSanctionsPressure() {
const previousState = await verifySeedKey(STATE_KEY).catch(() => null);
const previousIds = new Set(Array.isArray(previousState?.entryIds) ? previousState.entryIds.map((id) => String(id)) : []);
const hasPrevious = previousIds.size > 0;
const results = await Promise.all(OFAC_SOURCES.map((source) => fetchSource(source)));
const entries = results.flatMap((result) => result.entries);
const datasetDate = results.reduce((max, result) => Math.max(max, result.datasetDate || 0), 0);
if (hasPrevious) {
for (const entry of entries) {
entry.isNew = !previousIds.has(entry.id);
}
}
const sortedEntries = [...entries].sort(sortEntries);
const totalCount = entries.length;
const newEntryCount = hasPrevious ? entries.filter((entry) => entry.isNew).length : 0;
const vesselCount = entries.filter((entry) => entry.entityType === 'SANCTIONS_ENTITY_TYPE_VESSEL').length;
const aircraftCount = entries.filter((entry) => entry.entityType === 'SANCTIONS_ENTITY_TYPE_AIRCRAFT').length;
return {
fetchedAt: String(Date.now()),
datasetDate: String(datasetDate),
totalCount,
sdnCount: results[0]?.entries.length ?? 0,
consolidatedCount: results[1]?.entries.length ?? 0,
newEntryCount,
vesselCount,
aircraftCount,
countries: buildCountryPressure(entries),
programs: buildProgramPressure(entries),
entries: sortedEntries.slice(0, DEFAULT_RECENT_LIMIT),
_state: {
entryIds: entries.map((entry) => entry.id),
},
};
}
function validate(data) {
return (data?.totalCount ?? 0) > 0;
}
runSeed('sanctions', 'pressure', CANONICAL_KEY, fetchSanctionsPressure, {
ttlSeconds: CACHE_TTL,
validateFn: validate,
sourceVersion: 'ofac-sls-advanced-xml-v1',
recordCount: (data) => data.totalCount ?? 0,
extraKeys: [
{
key: STATE_KEY,
ttl: CACHE_TTL,
transform: (data) => data._state,
},
],
afterPublish: async (data, _ctx) => {
delete data._state;
},
});

View File

@@ -48,6 +48,7 @@ export const BOOTSTRAP_CACHE_KEYS: Record<string, string> = {
securityAdvisories: 'intelligence:advisories-bootstrap:v1', securityAdvisories: 'intelligence:advisories-bootstrap:v1',
forecasts: 'forecast:predictions:v2', forecasts: 'forecast:predictions:v2',
customsRevenue: 'trade:customs-revenue:v1', customsRevenue: 'trade:customs-revenue:v1',
sanctionsPressure: 'sanctions:pressure:v1',
}; };
export const BOOTSTRAP_TIERS: Record<string, 'slow' | 'fast'> = { export const BOOTSTRAP_TIERS: Record<string, 'slow' | 'fast'> = {
@@ -55,7 +56,7 @@ export const BOOTSTRAP_TIERS: Record<string, 'slow' | 'fast'> = {
minerals: 'slow', giving: 'slow', sectors: 'slow', minerals: 'slow', giving: 'slow', sectors: 'slow',
progressData: 'slow', renewableEnergy: 'slow', progressData: 'slow', renewableEnergy: 'slow',
etfFlows: 'slow', shippingRates: 'fast', wildfires: 'slow', etfFlows: 'slow', shippingRates: 'fast', wildfires: 'slow',
climateAnomalies: 'slow', radiationWatch: 'slow', cyberThreats: 'slow', techReadiness: 'slow', climateAnomalies: 'slow', sanctionsPressure: 'slow', radiationWatch: 'slow', cyberThreats: 'slow', techReadiness: 'slow',
theaterPosture: 'fast', naturalEvents: 'slow', theaterPosture: 'fast', naturalEvents: 'slow',
cryptoQuotes: 'slow', gulfQuotes: 'slow', stablecoinMarkets: 'slow', cryptoQuotes: 'slow', gulfQuotes: 'slow', stablecoinMarkets: 'slow',
unrestEvents: 'slow', ucdpEvents: 'slow', techEvents: 'slow', unrestEvents: 'slow', ucdpEvents: 'slow', techEvents: 'slow',

View File

@@ -89,6 +89,7 @@ const RPC_CACHE_TIER: Record<string, CacheTier> = {
'/api/giving/v1/get-giving-summary': 'static', '/api/giving/v1/get-giving-summary': 'static',
'/api/intelligence/v1/get-country-intel-brief': 'static', '/api/intelligence/v1/get-country-intel-brief': 'static',
'/api/climate/v1/list-climate-anomalies': 'static', '/api/climate/v1/list-climate-anomalies': 'static',
'/api/sanctions/v1/list-sanctions-pressure': 'static',
'/api/radiation/v1/list-radiation-observations': 'slow', '/api/radiation/v1/list-radiation-observations': 'slow',
'/api/research/v1/list-tech-events': 'static', '/api/research/v1/list-tech-events': 'static',
'/api/military/v1/get-usni-fleet-report': 'static', '/api/military/v1/get-usni-fleet-report': 'static',

View File

@@ -0,0 +1,7 @@
import type { SanctionsServiceHandler } from '../../../../src/generated/server/worldmonitor/sanctions/v1/service_server';
import { listSanctionsPressure } from './list-sanctions-pressure';
export const sanctionsHandler: SanctionsServiceHandler = {
listSanctionsPressure,
};

View File

@@ -0,0 +1,424 @@
import { XMLParser } from 'fast-xml-parser';
import type {
ListSanctionsPressureRequest,
ListSanctionsPressureResponse,
ProgramSanctionsPressure,
SanctionsEntityType,
SanctionsEntry,
SanctionsServiceHandler,
ServerContext,
CountrySanctionsPressure,
} from '../../../../src/generated/server/worldmonitor/sanctions/v1/service_server';
import { CHROME_UA } from '../../../_shared/constants';
import { cachedFetchJson, getCachedJson } from '../../../_shared/redis';
const REDIS_CACHE_KEY = 'sanctions:pressure:v1';
const REDIS_CACHE_TTL = 30 * 60;
const SEED_FRESHNESS_MS = 18 * 60 * 60 * 1000;
const DEFAULT_MAX_ITEMS = 25;
const MAX_ITEMS_LIMIT = 60;
const OFAC_TIMEOUT_MS = 12_000;
const PROGRAM_CODE_RE = /^[A-Z0-9][A-Z0-9-]{1,24}$/;
const OFAC_SOURCES = [
{ label: 'SDN', url: 'https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/sdn_advanced.xml' },
{ label: 'CONSOLIDATED', url: 'https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/cons_advanced.xml' },
] as const;
const XML_PARSER = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
removeNSPrefix: true,
parseTagValue: false,
trimValues: true,
});
type InternalEntry = SanctionsEntry;
function toInt64String(value: number): string {
return String(Math.max(0, Math.trunc(value)));
}
function listify<T>(value: T | T[] | null | undefined): T[] {
if (Array.isArray(value)) return value;
return value == null ? [] : [value];
}
function textValue(value: unknown): string {
if (value == null) return '';
if (typeof value === 'string') return value.trim();
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
if (typeof value === 'object') {
const obj = value as Record<string, unknown>;
if (typeof obj['#text'] === 'string') return obj['#text'].trim();
if (typeof obj.NamePartValue === 'string') return obj.NamePartValue.trim();
}
return '';
}
function buildEpoch(parts: Record<string, unknown> | undefined): number {
const year = Number(parts?.Year || 0);
if (!year) return 0;
const month = Math.max(1, Number(parts?.Month || 1));
const day = Math.max(1, Number(parts?.Day || 1));
return Date.UTC(year, month - 1, day);
}
function uniqueSorted(values: string[]): string[] {
return [...new Set(values.filter(Boolean).map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
}
function compactNote(value: string): string {
const note = value.replace(/\s+/g, ' ').trim();
if (!note) return '';
return note.length > 240 ? `${note.slice(0, 237)}...` : note;
}
function extractDocumentedName(documentedName: Record<string, unknown> | undefined): string {
const parts = listify(documentedName?.DocumentedNamePart as Record<string, unknown> | Record<string, unknown>[])
.map((part) => textValue((part as Record<string, unknown>)?.NamePartValue))
.filter(Boolean);
if (parts.length > 0) return parts.join(' ');
return textValue(documentedName);
}
function buildReferenceMaps(doc: Record<string, unknown>) {
const refs = (doc.ReferenceValueSets ?? {}) as Record<string, unknown>;
const areaCodes = new Map<string, { code: string; name: string }>();
for (const area of listify((refs.AreaCodeValues as Record<string, unknown> | undefined)?.AreaCode as Record<string, unknown> | Record<string, unknown>[])) {
areaCodes.set(String(area.ID || ''), {
code: textValue(area),
name: String(area.Description || '').trim(),
});
}
const featureTypes = new Map<string, string>();
for (const feature of listify((refs.FeatureTypeValues as Record<string, unknown> | undefined)?.FeatureType as Record<string, unknown> | Record<string, unknown>[])) {
featureTypes.set(String(feature.ID || ''), textValue(feature));
}
const legalBasis = new Map<string, string>();
for (const basis of listify((refs.LegalBasisValues as Record<string, unknown> | undefined)?.LegalBasis as Record<string, unknown> | Record<string, unknown>[])) {
legalBasis.set(String(basis.ID || ''), String(basis.LegalBasisShortRef || textValue(basis) || '').trim());
}
return { areaCodes, featureTypes, legalBasis };
}
function buildLocationMap(doc: Record<string, unknown>, areaCodes: Map<string, { code: string; name: string }>) {
const locations = new Map<string, { codes: string[]; names: string[] }>();
for (const location of listify(((doc.Locations as Record<string, unknown> | undefined)?.Location) as Record<string, unknown> | Record<string, unknown>[])) {
const ids = listify(location.LocationAreaCode as Record<string, unknown> | Record<string, unknown>[]).map((item) => String(item.AreaCodeID || ''));
const mapped = ids.map((id) => areaCodes.get(id)).filter((item): item is { code: string; name: string } => Boolean(item));
// Sort code/name as pairs so codes[i] always corresponds to names[i]
const pairs = [...new Map(mapped.map((item) => [item.code, item.name] as [string, string])).entries()]
.filter(([code]) => code.length > 0)
.sort(([a], [b]) => a.localeCompare(b));
locations.set(String(location.ID || ''), {
codes: pairs.map(([code]) => code),
names: pairs.map(([, name]) => name),
});
}
return locations;
}
function extractPartyName(profile: Record<string, unknown>): string {
const identities = listify(profile.Identity as Record<string, unknown> | Record<string, unknown>[]);
const aliases = identities.flatMap((identity) => listify(identity.Alias as Record<string, unknown> | Record<string, unknown>[]));
const primaryAlias = aliases.find((alias) => alias?.Primary === 'true')
|| aliases.find((alias) => alias?.AliasTypeID === '1403')
|| aliases[0];
return extractDocumentedName(primaryAlias?.DocumentedName as Record<string, unknown> | undefined);
}
function resolveEntityType(profile: Record<string, unknown>, featureTypes: Map<string, string>): SanctionsEntityType {
const subtype = String(profile.PartySubTypeID || '');
if (subtype === '1') return 'SANCTIONS_ENTITY_TYPE_VESSEL';
if (subtype === '2') return 'SANCTIONS_ENTITY_TYPE_AIRCRAFT';
const featureNames = listify(profile.Feature as Record<string, unknown> | Record<string, unknown>[])
.map((feature) => featureTypes.get(String(feature?.FeatureTypeID || '')) || '')
.filter(Boolean);
if (featureNames.some((name) => /birth|citizenship|nationality/i.test(name))) {
return 'SANCTIONS_ENTITY_TYPE_INDIVIDUAL';
}
return 'SANCTIONS_ENTITY_TYPE_ENTITY';
}
function extractPartyCountries(
profile: Record<string, unknown>,
featureTypes: Map<string, string>,
locations: Map<string, { codes: string[]; names: string[] }>,
): { countryCodes: string[]; countryNames: string[] } {
// Use a Map to deduplicate by code while preserving code→name alignment
const seen = new Map<string, string>();
for (const feature of listify(profile.Feature as Record<string, unknown> | Record<string, unknown>[])) {
const featureType = featureTypes.get(String(feature?.FeatureTypeID || '')) || '';
if (!/location/i.test(featureType)) continue;
for (const version of listify(feature.FeatureVersion as Record<string, unknown> | Record<string, unknown>[])) {
const locationIds = listify(version.VersionLocation as Record<string, unknown> | Record<string, unknown>[]).map((item) => String(item?.LocationID || ''));
for (const locationId of locationIds) {
const location = locations.get(locationId);
if (!location) continue;
location.codes.forEach((code, i) => {
if (code && !seen.has(code)) seen.set(code, location.names[i] ?? '');
});
}
}
}
const sorted = [...seen.entries()].sort(([a], [b]) => a.localeCompare(b));
return {
countryCodes: sorted.map(([c]) => c),
countryNames: sorted.map(([, n]) => n),
};
}
function buildPartyMap(
doc: Record<string, unknown>,
featureTypes: Map<string, string>,
locations: Map<string, { codes: string[]; names: string[] }>,
) {
const parties = new Map<string, { name: string; entityType: SanctionsEntityType; countryCodes: string[]; countryNames: string[] }>();
for (const distinctParty of listify(((doc.DistinctParties as Record<string, unknown> | undefined)?.DistinctParty) as Record<string, unknown> | Record<string, unknown>[])) {
const profile = distinctParty.Profile as Record<string, unknown> | undefined;
const profileId = String(profile?.ID || distinctParty.FixedRef || '');
if (!profile || !profileId) continue;
parties.set(profileId, {
name: extractPartyName(profile),
entityType: resolveEntityType(profile, featureTypes),
...extractPartyCountries(profile, featureTypes, locations),
});
}
return parties;
}
function extractPrograms(entry: Record<string, unknown>): string[] {
const directPrograms = listify(entry.SanctionsMeasure as Record<string, unknown> | Record<string, unknown>[])
.map((measure) => textValue(measure?.Comment))
.filter((value) => PROGRAM_CODE_RE.test(value));
return uniqueSorted(directPrograms);
}
function extractEffectiveAt(entry: Record<string, unknown>): number {
const dates: number[] = [];
for (const event of listify(entry.EntryEvent as Record<string, unknown> | Record<string, unknown>[])) {
const epoch = buildEpoch(event.Date as Record<string, unknown> | undefined);
if (epoch > 0) dates.push(epoch);
}
for (const measure of listify(entry.SanctionsMeasure as Record<string, unknown> | Record<string, unknown>[])) {
const datePeriod = measure.DatePeriod as Record<string, unknown> | undefined;
const epoch = buildEpoch((datePeriod?.Start as Record<string, unknown> | undefined)?.From as Record<string, unknown> | undefined || datePeriod?.Start as Record<string, unknown> | undefined);
if (epoch > 0) dates.push(epoch);
}
return dates.length > 0 ? Math.max(...dates) : 0;
}
function extractNote(entry: Record<string, unknown>, legalBasis: Map<string, string>): string {
const comments = listify(entry.SanctionsMeasure as Record<string, unknown> | Record<string, unknown>[])
.map((measure) => textValue(measure?.Comment))
.filter((value) => value && !PROGRAM_CODE_RE.test(value));
if (comments.length > 0) return compactNote(comments[0]!);
const legal = listify(entry.EntryEvent as Record<string, unknown> | Record<string, unknown>[])
.map((event) => legalBasis.get(String(event?.LegalBasisID || '')) || '')
.filter(Boolean);
return compactNote(legal[0] || '');
}
function buildEntriesForDocument(doc: Record<string, unknown>, sourceLabel: 'SDN' | 'CONSOLIDATED') {
const { areaCodes, featureTypes, legalBasis } = buildReferenceMaps(doc);
const locations = buildLocationMap(doc, areaCodes);
const parties = buildPartyMap(doc, featureTypes, locations);
const datasetDate = buildEpoch(doc.DateOfIssue as Record<string, unknown> | undefined);
const entries: InternalEntry[] = [];
for (const entry of listify(((doc.SanctionsEntries as Record<string, unknown> | undefined)?.SanctionsEntry) as Record<string, unknown> | Record<string, unknown>[])) {
const profileId = String(entry.ProfileID || '');
const party = parties.get(profileId);
const name = party?.name || 'Unnamed designation';
const programs = extractPrograms(entry);
entries.push({
id: `${sourceLabel}:${String(entry.ID || profileId || name)}`,
name,
entityType: party?.entityType || 'SANCTIONS_ENTITY_TYPE_ENTITY',
countryCodes: party?.countryCodes ?? [],
countryNames: party?.countryNames ?? [],
programs: programs.length > 0 ? programs : [sourceLabel],
sourceLists: [sourceLabel],
effectiveAt: toInt64String(extractEffectiveAt(entry)),
isNew: false,
note: extractNote(entry, legalBasis),
});
}
return { entries, datasetDate };
}
function sortEntries(a: InternalEntry, b: InternalEntry): number {
return (Number(b.isNew) - Number(a.isNew))
|| (Number(b.effectiveAt) - Number(a.effectiveAt))
|| a.name.localeCompare(b.name);
}
function buildCountryPressure(entries: InternalEntry[]): CountrySanctionsPressure[] {
const map = new Map<string, CountrySanctionsPressure>();
for (const entry of entries) {
const codes = entry.countryCodes.length > 0 ? entry.countryCodes : ['XX'];
const names = entry.countryNames.length > 0 ? entry.countryNames : ['Unknown'];
codes.forEach((code, index) => {
const key = `${code}:${names[index] || names[0] || 'Unknown'}`;
const current = map.get(key) || {
countryCode: code,
countryName: names[index] || names[0] || 'Unknown',
entryCount: 0,
newEntryCount: 0,
vesselCount: 0,
aircraftCount: 0,
};
current.entryCount += 1;
if (entry.isNew) current.newEntryCount += 1;
if (entry.entityType === 'SANCTIONS_ENTITY_TYPE_VESSEL') current.vesselCount += 1;
if (entry.entityType === 'SANCTIONS_ENTITY_TYPE_AIRCRAFT') current.aircraftCount += 1;
map.set(key, current);
});
}
return [...map.values()]
.sort((a, b) => b.newEntryCount - a.newEntryCount || b.entryCount - a.entryCount || a.countryName.localeCompare(b.countryName))
.slice(0, 12);
}
function buildProgramPressure(entries: InternalEntry[]): ProgramSanctionsPressure[] {
const map = new Map<string, ProgramSanctionsPressure>();
for (const entry of entries) {
const programs = entry.programs.length > 0 ? entry.programs : ['UNSPECIFIED'];
for (const program of programs) {
const current = map.get(program) || { program, entryCount: 0, newEntryCount: 0 };
current.entryCount += 1;
if (entry.isNew) current.newEntryCount += 1;
map.set(program, current);
}
}
return [...map.values()]
.sort((a, b) => b.newEntryCount - a.newEntryCount || b.entryCount - a.entryCount || a.program.localeCompare(b.program))
.slice(0, 12);
}
async function fetchSource(source: typeof OFAC_SOURCES[number]) {
const response = await fetch(source.url, {
headers: { 'User-Agent': CHROME_UA },
signal: AbortSignal.timeout(OFAC_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`OFAC ${source.label} HTTP ${response.status}`);
}
const xml = await response.text();
const parsed = XML_PARSER.parse(xml)?.Sanctions as Record<string, unknown> | undefined;
if (!parsed) throw new Error(`OFAC ${source.label} parse returned no Sanctions root`);
return buildEntriesForDocument(parsed, source.label);
}
function trimResponse(data: ListSanctionsPressureResponse, maxItems: number): ListSanctionsPressureResponse {
// Destructure out _state which may be present in seeded Redis payloads during the window
// between atomicPublish and afterPublish deletion
const { _state: _discarded, ...rest } = data as ListSanctionsPressureResponse & { _state?: unknown };
return {
...rest,
fetchedAt: String(data.fetchedAt ?? '0'),
datasetDate: String(data.datasetDate ?? '0'),
entries: (data.entries ?? []).map((entry) => ({
...entry,
effectiveAt: String(entry.effectiveAt ?? '0'),
})).slice(0, maxItems),
};
}
async function trySeededData(maxItems: number): Promise<ListSanctionsPressureResponse | null> {
try {
const [data, meta] = await Promise.all([
getCachedJson(REDIS_CACHE_KEY, true) as Promise<ListSanctionsPressureResponse | null>,
getCachedJson('seed-meta:sanctions:pressure', true) as Promise<{ fetchedAt?: number } | null>,
]);
if (!data || !meta?.fetchedAt) return null;
if (Date.now() - meta.fetchedAt > SEED_FRESHNESS_MS) return null;
return trimResponse(data, maxItems);
} catch {
return null;
}
}
async function collectPressure(maxItems: number): Promise<ListSanctionsPressureResponse> {
const results = await Promise.all(OFAC_SOURCES.map((source) => fetchSource(source)));
const entries = results.flatMap((result) => result.entries).sort(sortEntries);
const totalCount = entries.length;
return {
fetchedAt: toInt64String(Date.now()),
datasetDate: toInt64String(results.reduce((max, result) => Math.max(max, result.datasetDate || 0), 0)),
totalCount,
sdnCount: results[0]?.entries.length ?? 0,
consolidatedCount: results[1]?.entries.length ?? 0,
newEntryCount: 0,
vesselCount: entries.filter((entry) => entry.entityType === 'SANCTIONS_ENTITY_TYPE_VESSEL').length,
aircraftCount: entries.filter((entry) => entry.entityType === 'SANCTIONS_ENTITY_TYPE_AIRCRAFT').length,
countries: buildCountryPressure(entries),
programs: buildProgramPressure(entries),
entries: entries.slice(0, maxItems),
};
}
function clampMaxItems(value: number): number {
if (!Number.isFinite(value) || value <= 0) return DEFAULT_MAX_ITEMS;
return Math.min(Math.max(Math.trunc(value), 1), MAX_ITEMS_LIMIT);
}
function emptyResponse(): ListSanctionsPressureResponse {
return {
entries: [],
countries: [],
programs: [],
fetchedAt: '0',
datasetDate: '0',
totalCount: 0,
sdnCount: 0,
consolidatedCount: 0,
newEntryCount: 0,
vesselCount: 0,
aircraftCount: 0,
};
}
export const listSanctionsPressure: SanctionsServiceHandler['listSanctionsPressure'] = async (
_ctx: ServerContext,
req: ListSanctionsPressureRequest,
): Promise<ListSanctionsPressureResponse> => {
const maxItems = clampMaxItems(req.maxItems);
try {
const seeded = await trySeededData(maxItems);
if (seeded) return seeded;
return await cachedFetchJson<ListSanctionsPressureResponse>(
`${REDIS_CACHE_KEY}:live:${maxItems}`,
REDIS_CACHE_TTL,
async () => collectPressure(maxItems),
) ?? emptyResponse();
} catch {
return emptyResponse();
}
};

View File

@@ -1,6 +1,7 @@
import type { InternetOutage, SocialUnrestEvent, MilitaryFlight, MilitaryFlightCluster, MilitaryVessel, MilitaryVesselCluster, USNIFleetReport, PanelConfig, MapLayers, NewsItem, MarketData, ClusteredEvent, CyberThreat, Monitor } from '@/types'; import type { InternetOutage, SocialUnrestEvent, MilitaryFlight, MilitaryFlightCluster, MilitaryVessel, MilitaryVesselCluster, USNIFleetReport, PanelConfig, MapLayers, NewsItem, MarketData, ClusteredEvent, CyberThreat, Monitor } from '@/types';
import type { AirportDelayAlert, PositionSample } from '@/services/aviation'; import type { AirportDelayAlert, PositionSample } from '@/services/aviation';
import type { IranEvent } from '@/generated/client/worldmonitor/conflict/v1/service_client'; import type { IranEvent } from '@/generated/client/worldmonitor/conflict/v1/service_client';
import type { SanctionsPressureResult } from '@/services/sanctions-pressure';
import type { RadiationWatchResult } from '@/services/radiation'; import type { RadiationWatchResult } from '@/services/radiation';
import type { SecurityAdvisory } from '@/services/security-advisories'; import type { SecurityAdvisory } from '@/services/security-advisories';
import type { Earthquake } from '@/services/earthquakes'; import type { Earthquake } from '@/services/earthquakes';
@@ -18,6 +19,7 @@ export interface IntelligenceCache {
iranEvents?: IranEvent[]; iranEvents?: IranEvent[];
orefAlerts?: { alertCount: number; historyCount24h: number }; orefAlerts?: { alertCount: number; historyCount24h: number };
advisories?: SecurityAdvisory[]; advisories?: SecurityAdvisory[];
sanctions?: SanctionsPressureResult;
radiation?: RadiationWatchResult; radiation?: RadiationWatchResult;
imageryScenes?: Array<{ id: string; satellite: string; datetime: string; resolutionM: number; mode: string; geometryGeojson: string; previewUrl: string; assetUrl: string }>; imageryScenes?: Array<{ id: string; satellite: string; datetime: string; resolutionM: number; mode: string; geometryGeojson: string; previewUrl: string; assetUrl: string }>;
} }

View File

@@ -60,6 +60,7 @@ import {
fetchShippingRates, fetchShippingRates,
fetchChokepointStatus, fetchChokepointStatus,
fetchCriticalMinerals, fetchCriticalMinerals,
fetchSanctionsPressure,
fetchRadiationWatch, fetchRadiationWatch,
} from '@/services'; } from '@/services';
import { getMarketWatchlistEntries } from '@/services/market-watchlist'; import { getMarketWatchlistEntries } from '@/services/market-watchlist';
@@ -474,6 +475,9 @@ export class DataLoaderManager implements AppModule {
if (SITE_VARIANT !== 'happy' && (this.ctx.mapLayers.techEvents || SITE_VARIANT === 'tech')) tasks.push({ name: 'techEvents', task: runGuarded('techEvents', () => this.loadTechEvents()) }); if (SITE_VARIANT !== 'happy' && (this.ctx.mapLayers.techEvents || SITE_VARIANT === 'tech')) tasks.push({ name: 'techEvents', task: runGuarded('techEvents', () => this.loadTechEvents()) });
if (SITE_VARIANT !== 'happy' && this.ctx.mapLayers.satellites && this.ctx.map?.isGlobeMode?.()) tasks.push({ name: 'satellites', task: runGuarded('satellites', () => this.loadSatellites()) }); if (SITE_VARIANT !== 'happy' && this.ctx.mapLayers.satellites && this.ctx.map?.isGlobeMode?.()) tasks.push({ name: 'satellites', task: runGuarded('satellites', () => this.loadSatellites()) });
if (SITE_VARIANT !== 'happy' && this.ctx.mapLayers.webcams) tasks.push({ name: 'webcams', task: runGuarded('webcams', () => this.loadWebcams()) }); if (SITE_VARIANT !== 'happy' && this.ctx.mapLayers.webcams) tasks.push({ name: 'webcams', task: runGuarded('webcams', () => this.loadWebcams()) });
if (SITE_VARIANT !== 'happy' && (this.ctx.panels['sanctions-pressure'] || this.ctx.mapLayers.sanctions)) {
tasks.push({ name: 'sanctions', task: runGuarded('sanctions', () => this.loadSanctionsPressure()) });
}
if (SITE_VARIANT !== 'happy' && (this.ctx.panels['radiation-watch'] || this.ctx.mapLayers.radiationWatch)) { if (SITE_VARIANT !== 'happy' && (this.ctx.panels['radiation-watch'] || this.ctx.mapLayers.radiationWatch)) {
tasks.push({ name: 'radiation', task: runGuarded('radiation', () => this.loadRadiationWatch()) }); tasks.push({ name: 'radiation', task: runGuarded('radiation', () => this.loadRadiationWatch()) });
} }
@@ -579,6 +583,9 @@ export class DataLoaderManager implements AppModule {
case 'webcams': case 'webcams':
await this.loadWebcams(); await this.loadWebcams();
break; break;
case 'sanctions':
await this.loadSanctionsPressure();
break;
case 'radiationWatch': case 'radiationWatch':
await this.loadRadiationWatch(); await this.loadRadiationWatch();
break; break;
@@ -2673,6 +2680,25 @@ export class DataLoaderManager implements AppModule {
} }
} }
async loadSanctionsPressure(): Promise<void> {
try {
const result = await fetchSanctionsPressure();
this.callPanel('sanctions-pressure', 'setData', result);
this.ctx.intelligenceCache.sanctions = result;
signalAggregator.ingestSanctionsPressure(result.countries);
if (result.totalCount > 0) {
dataFreshness.recordUpdate('sanctions_pressure', result.totalCount);
this.ctx.statusPanel?.updateApi('OFAC', { status: result.newEntryCount > 0 ? 'warning' : 'ok' });
} else {
this.ctx.statusPanel?.updateApi('OFAC', { status: 'error' });
}
} catch (error) {
console.error('[App] Sanctions pressure fetch failed:', error);
dataFreshness.recordError('sanctions_pressure', String(error));
this.ctx.statusPanel?.updateApi('OFAC', { status: 'error' });
}
}
async loadRadiationWatch(): Promise<void> { async loadRadiationWatch(): Promise<void> {
try { try {
const result = await fetchRadiationWatch(); const result = await fetchRadiationWatch();

View File

@@ -33,6 +33,7 @@ import {
InvestmentsPanel, InvestmentsPanel,
TradePolicyPanel, TradePolicyPanel,
SupplyChainPanel, SupplyChainPanel,
SanctionsPressurePanel,
GulfEconomiesPanel, GulfEconomiesPanel,
WorldClockPanel, WorldClockPanel,
AirlineIntelPanel, AirlineIntelPanel,
@@ -565,6 +566,7 @@ export class PanelLayoutManager implements AppModule {
this.createPanel('economic', () => new EconomicPanel()); this.createPanel('economic', () => new EconomicPanel());
this.createPanel('trade-policy', () => new TradePolicyPanel()); this.createPanel('trade-policy', () => new TradePolicyPanel());
this.createPanel('sanctions-pressure', () => new SanctionsPressurePanel());
this.createPanel('supply-chain', () => new SupplyChainPanel()); this.createPanel('supply-chain', () => new SupplyChainPanel());
this.createNewsPanel('africa', 'panels.africa'); this.createNewsPanel('africa', 'panels.africa');

View File

@@ -0,0 +1,152 @@
import { Panel } from './Panel';
import type { CountrySanctionsPressure, ProgramSanctionsPressure, SanctionsEntry, SanctionsPressureResult } from '@/services/sanctions-pressure';
import { escapeHtml } from '@/utils/sanitize';
export class SanctionsPressurePanel extends Panel {
private data: SanctionsPressureResult | null = null;
constructor() {
super({
id: 'sanctions-pressure',
title: 'Sanctions Pressure',
showCount: true,
trackActivity: true,
defaultRowSpan: 2,
infoTooltip: 'Structured OFAC sanctions pressure built from seeded SDN and Consolidated List exports. This panel answers where sanctions pressure is concentrated, what is newly designated, and which programs are driving the latest pressure.',
});
this.showLoading('Loading sanctions pressure...');
}
public setData(data: SanctionsPressureResult): void {
this.data = data;
this.setCount(data.totalCount);
this.render();
}
private render(): void {
if (!this.data) {
this.setContent('<div class="economic-empty">No sanctions pressure available.</div>');
return;
}
const data = this.data;
const topCountry = data.countries[0];
const topProgram = data.programs[0];
const summaryHtml = `
<div class="sanctions-summary">
${this.renderSummaryCard('New', data.newEntryCount, data.newEntryCount > 0 ? 'highlight' : '')}
${this.renderSummaryCard('Countries', data.countries.length)}
${this.renderSummaryCard('Programs', data.programs.length)}
${this.renderSummaryCard('Vessels', data.vesselCount)}
${this.renderSummaryCard('Aircraft', data.aircraftCount)}
${this.renderSummaryCard('Source Mix', `${data.sdnCount}/${data.consolidatedCount}`, 'muted')}
</div>
<div class="sanctions-headlines">
<div class="sanctions-headline">
<span class="sanctions-headline-label">Top country</span>
<span class="sanctions-headline-value">${topCountry ? `${escapeHtml(topCountry.countryName)} (${topCountry.entryCount})` : 'No country attribution'}</span>
</div>
<div class="sanctions-headline">
<span class="sanctions-headline-label">Top program</span>
<span class="sanctions-headline-value">${topProgram ? `${escapeHtml(topProgram.program)} (${topProgram.entryCount})` : 'No program breakdown'}</span>
</div>
</div>
`;
const countriesHtml = data.countries.length > 0
? data.countries.slice(0, 8).map((country) => this.renderCountryRow(country)).join('')
: '<div class="economic-empty">No country pressure breakdown available.</div>';
const programsHtml = data.programs.length > 0
? data.programs.slice(0, 8).map((program) => this.renderProgramRow(program)).join('')
: '<div class="economic-empty">No program pressure breakdown available.</div>';
const entriesHtml = data.entries.length > 0
? data.entries.slice(0, 10).map((entry) => this.renderEntryRow(entry)).join('')
: '<div class="economic-empty">No recent designations available.</div>';
const footer = [
`Updated ${data.fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`,
data.datasetDate ? `dataset ${data.datasetDate.toISOString().slice(0, 10)}` : '',
'Source: OFAC',
].filter(Boolean).join(' · ');
this.setContent(`
<div class="sanctions-panel-content">
${summaryHtml}
<div class="sanctions-sections">
<div class="sanctions-section">
<div class="sanctions-section-title">Top countries</div>
<div class="sanctions-list">${countriesHtml}</div>
</div>
<div class="sanctions-section">
<div class="sanctions-section-title">Top programs</div>
<div class="sanctions-list">${programsHtml}</div>
</div>
<div class="sanctions-section">
<div class="sanctions-section-title">Recent designations</div>
<div class="sanctions-list">${entriesHtml}</div>
</div>
</div>
<div class="economic-footer">${escapeHtml(footer)}</div>
</div>
`);
}
private renderSummaryCard(label: string, value: string | number, tone = ''): string {
return `
<div class="sanctions-summary-card ${tone ? `sanctions-summary-card-${tone}` : ''}">
<span class="sanctions-summary-label">${escapeHtml(label)}</span>
<span class="sanctions-summary-value">${escapeHtml(String(value))}</span>
</div>
`;
}
private renderCountryRow(country: CountrySanctionsPressure): string {
const flags: string[] = [];
if (country.newEntryCount > 0) flags.push(`<span class="sanctions-pill sanctions-pill-new">+${country.newEntryCount} new</span>`);
if (country.vesselCount > 0) flags.push(`<span class="sanctions-pill">🚢 ${country.vesselCount}</span>`);
if (country.aircraftCount > 0) flags.push(`<span class="sanctions-pill">✈ ${country.aircraftCount}</span>`);
return `
<div class="sanctions-row">
<div class="sanctions-row-main">
<div class="sanctions-row-title">${escapeHtml(country.countryName)}</div>
<div class="sanctions-row-meta">${escapeHtml(country.countryCode)} · ${country.entryCount} designations</div>
</div>
<div class="sanctions-row-flags">${flags.join('')}</div>
</div>
`;
}
private renderProgramRow(program: ProgramSanctionsPressure): string {
return `
<div class="sanctions-row">
<div class="sanctions-row-main">
<div class="sanctions-row-title">${escapeHtml(program.program)}</div>
<div class="sanctions-row-meta">${program.entryCount} designations</div>
</div>
<div class="sanctions-row-flags">
${program.newEntryCount > 0 ? `<span class="sanctions-pill sanctions-pill-new">+${program.newEntryCount} new</span>` : ''}
</div>
</div>
`;
}
private renderEntryRow(entry: SanctionsEntry): string {
const location = entry.countryNames[0] || entry.countryCodes[0] || 'Unattributed';
const program = entry.programs[0] || 'Program';
const note = entry.note ? `<div class="sanctions-entry-note">${escapeHtml(entry.note)}</div>` : '';
const effective = entry.effectiveAt ? entry.effectiveAt.toISOString().slice(0, 10) : 'undated';
return `
<div class="sanctions-entry">
<div class="sanctions-entry-top">
<span class="sanctions-entry-name">${escapeHtml(entry.name)}</span>
<span class="sanctions-pill sanctions-pill-type">${escapeHtml(entry.entityType)}</span>
${entry.isNew ? '<span class="sanctions-pill sanctions-pill-new">new</span>' : ''}
</div>
<div class="sanctions-entry-meta">${escapeHtml(location)} · ${escapeHtml(program)} · ${escapeHtml(effective)}</div>
${note}
</div>
`;
}
}

View File

@@ -135,6 +135,7 @@ export class SignalModal {
cii_spike: '📊', cii_spike: '📊',
convergence: '🌍', convergence: '🌍',
cascade: '⚡', cascade: '⚡',
sanctions: '🚫',
radiation: '☢️', radiation: '☢️',
composite: '🔗', composite: '🔗',
}; };
@@ -206,6 +207,33 @@ export class SignalModal {
`; `;
} }
if (alert.components.sanctions) {
const sanctions = alert.components.sanctions;
detailsHtml += `
<div class="signal-context-item">
<span class="context-label">Country</span>
<span class="context-value">${escapeHtml(sanctions.countryName)} (${escapeHtml(sanctions.countryCode)})</span>
</div>
<div class="signal-context-item">
<span class="context-label">Pressure</span>
<span class="context-value">${sanctions.entryCount} designations${sanctions.newEntryCount > 0 ? ` · +${sanctions.newEntryCount} new` : ''}</span>
</div>
<div class="signal-context-item">
<span class="context-label">Top program</span>
<span class="context-value">${escapeHtml(sanctions.topProgram)} (${sanctions.topProgramCount})</span>
</div>
<div class="signal-context-item">
<span class="context-label">Vessels / aircraft</span>
<span class="context-value">${sanctions.vesselCount} / ${sanctions.aircraftCount}</span>
</div>
<div class="signal-context-item">
<span class="context-label">Dataset size</span>
<span class="context-value">${sanctions.totalCount}${sanctions.datasetDate ? ` · ${new Date(sanctions.datasetDate).toISOString().slice(0, 10)}` : ''}</span>
</div>
`;
}
if (alert.components.radiation) { if (alert.components.radiation) {
const radiation = alert.components.radiation; const radiation = alert.components.radiation;
detailsHtml += ` detailsHtml += `

View File

@@ -39,7 +39,7 @@ const WORLD_FEEDS = new Set([
const WORLD_APIS = new Set([ const WORLD_APIS = new Set([
'RSS2JSON', 'Finnhub', 'CoinGecko', 'Polymarket', 'USGS', 'FRED', 'RSS2JSON', 'Finnhub', 'CoinGecko', 'Polymarket', 'USGS', 'FRED',
'AISStream', 'GDELT Doc', 'EIA', 'USASpending', 'PizzINT', 'FIRMS', 'AISStream', 'GDELT Doc', 'EIA', 'USASpending', 'PizzINT', 'FIRMS',
'Cyber Threats API', 'BIS', 'WTO', 'SupplyChain' 'Cyber Threats API', 'BIS', 'WTO', 'SupplyChain', 'OFAC'
]); ]);
import { t } from '../services/i18n'; import { t } from '../services/i18n';

View File

@@ -209,6 +209,7 @@ export class StrategicRiskPanel extends Panel {
case 'convergence': return '🎯'; case 'convergence': return '🎯';
case 'cii_spike': return '📊'; case 'cii_spike': return '📊';
case 'cascade': return '🔗'; case 'cascade': return '🔗';
case 'sanctions': return '🚫';
case 'radiation': return '☢️'; case 'radiation': return '☢️';
case 'composite': return '⚠️'; case 'composite': return '⚠️';
default: return '📍'; default: return '📍';

View File

@@ -47,6 +47,7 @@ export * from './UnifiedSettings';
export * from './TradePolicyPanel'; export * from './TradePolicyPanel';
export * from './SupplyChainPanel'; export * from './SupplyChainPanel';
export * from './SecurityAdvisoriesPanel'; export * from './SecurityAdvisoriesPanel';
export * from './SanctionsPressurePanel';
export * from './RadiationWatchPanel'; export * from './RadiationWatchPanel';
export * from './OrefSirensPanel'; export * from './OrefSirensPanel';
export * from './TelegramIntelPanel'; export * from './TelegramIntelPanel';

View File

@@ -108,6 +108,7 @@ export const COMMANDS: Command[] = [
{ id: 'panel:markets', keywords: ['markets', 'stocks', 'indices'], label: 'Panel: Markets', icon: '\u{1F4C8}', category: 'panels' }, { id: 'panel:markets', keywords: ['markets', 'stocks', 'indices'], label: 'Panel: Markets', icon: '\u{1F4C8}', category: 'panels' },
{ id: 'panel:economic', keywords: ['economic', 'economy', 'fred'], label: 'Panel: Economic Indicators', icon: '\u{1F4CA}', category: 'panels' }, { id: 'panel:economic', keywords: ['economic', 'economy', 'fred'], label: 'Panel: Economic Indicators', icon: '\u{1F4CA}', category: 'panels' },
{ id: 'panel:trade-policy', keywords: ['trade', 'tariffs', 'wto', 'trade policy', 'sanctions', 'restrictions'], label: 'Panel: Trade Policy', icon: '\u{1F4CA}', category: 'panels' }, { id: 'panel:trade-policy', keywords: ['trade', 'tariffs', 'wto', 'trade policy', 'sanctions', 'restrictions'], label: 'Panel: Trade Policy', icon: '\u{1F4CA}', category: 'panels' },
{ id: 'panel:sanctions-pressure', keywords: ['sanctions pressure', 'ofac', 'designation', 'sanctions'], label: 'Panel: Sanctions Pressure', icon: '\u{1F6AB}', category: 'panels' },
{ id: 'panel:supply-chain', keywords: ['supply chain', 'shipping', 'chokepoint', 'minerals', 'freight', 'logistics'], label: 'Panel: Supply Chain', icon: '\u{1F6A2}', category: 'panels' }, { id: 'panel:supply-chain', keywords: ['supply chain', 'shipping', 'chokepoint', 'minerals', 'freight', 'logistics'], label: 'Panel: Supply Chain', icon: '\u{1F6A2}', category: 'panels' },
{ id: 'panel:finance', keywords: ['financial', 'finance news'], label: 'Panel: Financial', icon: '\u{1F4B5}', category: 'panels' }, { id: 'panel:finance', keywords: ['financial', 'finance news'], label: 'Panel: Financial', icon: '\u{1F4B5}', category: 'panels' },
{ id: 'panel:tech', keywords: ['technology', 'tech news'], label: 'Panel: Technology', icon: '\u{1F4BB}', category: 'panels' }, { id: 'panel:tech', keywords: ['technology', 'tech news'], label: 'Panel: Technology', icon: '\u{1F4BB}', category: 'panels' },

View File

@@ -61,6 +61,7 @@ const FULL_PANELS: Record<string, PanelConfig> = {
climate: { name: 'Climate Anomalies', enabled: true, priority: 2 }, climate: { name: 'Climate Anomalies', enabled: true, priority: 2 },
'population-exposure': { name: 'Population Exposure', enabled: true, priority: 2 }, 'population-exposure': { name: 'Population Exposure', enabled: true, priority: 2 },
'security-advisories': { name: 'Security Advisories', enabled: true, priority: 2 }, 'security-advisories': { name: 'Security Advisories', enabled: true, priority: 2 },
'sanctions-pressure': { name: 'Sanctions Pressure', enabled: true, priority: 2 },
'radiation-watch': { name: 'Radiation Watch', enabled: true, priority: 2 }, 'radiation-watch': { name: 'Radiation Watch', enabled: true, priority: 2 },
'oref-sirens': { name: 'Israel Sirens', enabled: true, priority: 2, ...(_desktop && { premium: 'locked' as const }) }, 'oref-sirens': { name: 'Israel Sirens', enabled: true, priority: 2, ...(_desktop && { premium: 'locked' as const }) },
'telegram-intel': { name: 'Telegram Intel', enabled: true, priority: 2, ...(_desktop && { premium: 'locked' as const }) }, 'telegram-intel': { name: 'Telegram Intel', enabled: true, priority: 2, ...(_desktop && { premium: 'locked' as const }) },
@@ -377,6 +378,7 @@ const FINANCE_PANELS: Record<string, PanelConfig> = {
centralbanks: { name: 'Central Bank Watch', enabled: true, priority: 1 }, centralbanks: { name: 'Central Bank Watch', enabled: true, priority: 1 },
economic: { name: 'Economic Data', enabled: true, priority: 1 }, economic: { name: 'Economic Data', enabled: true, priority: 1 },
'trade-policy': { name: 'Trade Policy', enabled: true, priority: 1 }, 'trade-policy': { name: 'Trade Policy', enabled: true, priority: 1 },
'sanctions-pressure': { name: 'Sanctions Pressure', enabled: true, priority: 1 },
'supply-chain': { name: 'Supply Chain', enabled: true, priority: 1 }, 'supply-chain': { name: 'Supply Chain', enabled: true, priority: 1 },
'economic-news': { name: 'Economic News', enabled: true, priority: 2 }, 'economic-news': { name: 'Economic News', enabled: true, priority: 2 },
ipo: { name: 'IPOs, Earnings & M&A', enabled: true, priority: 1 }, ipo: { name: 'IPOs, Earnings & M&A', enabled: true, priority: 1 },
@@ -675,6 +677,7 @@ const COMMODITY_PANELS: Record<string, PanelConfig> = {
heatmap: { name: 'Sector Heatmap', enabled: true, priority: 1 }, heatmap: { name: 'Sector Heatmap', enabled: true, priority: 1 },
'macro-signals': { name: 'Market Radar', enabled: true, priority: 1 }, 'macro-signals': { name: 'Market Radar', enabled: true, priority: 1 },
'trade-policy': { name: 'Trade Policy', enabled: true, priority: 1 }, 'trade-policy': { name: 'Trade Policy', enabled: true, priority: 1 },
'sanctions-pressure': { name: 'Sanctions Pressure', enabled: true, priority: 1 },
economic: { name: 'Economic Indicators', enabled: true, priority: 1 }, economic: { name: 'Economic Indicators', enabled: true, priority: 1 },
'gulf-economies': { name: 'Gulf & OPEC Economies', enabled: true, priority: 1 }, 'gulf-economies': { name: 'Gulf & OPEC Economies', enabled: true, priority: 1 },
'gcc-investments': { name: 'GCC Resource Investments', enabled: true, priority: 2 }, 'gcc-investments': { name: 'GCC Resource Investments', enabled: true, priority: 2 },
@@ -849,6 +852,7 @@ export const LAYER_TO_SOURCE: Partial<Record<keyof MapLayers, DataSourceId[]>> =
ucdpEvents: ['ucdp_events'], ucdpEvents: ['ucdp_events'],
displacement: ['unhcr'], displacement: ['unhcr'],
climate: ['climate'], climate: ['climate'],
sanctions: ['sanctions_pressure'],
radiationWatch: ['radiation'], radiationWatch: ['radiation'],
}; };
@@ -884,7 +888,7 @@ export const PANEL_CATEGORY_MAP: Record<string, { labelKey: string; panelKeys: s
}, },
marketsFinance: { marketsFinance: {
labelKey: 'header.panelCatMarketsFinance', labelKey: 'header.panelCatMarketsFinance',
panelKeys: ['commodities', 'markets', 'economic', 'trade-policy', 'supply-chain', 'finance', 'polymarket', 'macro-signals', 'gulf-economies', 'etf-flows', 'stablecoins', 'crypto', 'heatmap'], panelKeys: ['commodities', 'markets', 'economic', 'trade-policy', 'sanctions-pressure', 'supply-chain', 'finance', 'polymarket', 'macro-signals', 'gulf-economies', 'etf-flows', 'stablecoins', 'crypto', 'heatmap'],
variants: ['full'], variants: ['full'],
}, },
topical: { topical: {
@@ -916,7 +920,7 @@ export const PANEL_CATEGORY_MAP: Record<string, { labelKey: string; panelKeys: s
}, },
techMarkets: { techMarkets: {
labelKey: 'header.panelCatMarkets', labelKey: 'header.panelCatMarkets',
panelKeys: ['markets', 'finance', 'crypto', 'economic', 'polymarket', 'macro-signals', 'etf-flows', 'stablecoins', 'layoffs', 'monitors', 'world-clock'], panelKeys: ['markets', 'finance', 'crypto', 'economic', 'sanctions-pressure', 'polymarket', 'macro-signals', 'etf-flows', 'stablecoins', 'layoffs', 'monitors', 'world-clock'],
variants: ['tech'], variants: ['tech'],
}, },
@@ -943,7 +947,7 @@ export const PANEL_CATEGORY_MAP: Record<string, { labelKey: string; panelKeys: s
}, },
centralBanksEcon: { centralBanksEcon: {
labelKey: 'header.panelCatCentralBanks', labelKey: 'header.panelCatCentralBanks',
panelKeys: ['centralbanks', 'economic', 'trade-policy', 'supply-chain', 'economic-news'], panelKeys: ['centralbanks', 'economic', 'trade-policy', 'sanctions-pressure', 'supply-chain', 'economic-news'],
variants: ['finance'], variants: ['finance'],
}, },
dealsInstitutional: { dealsInstitutional: {
@@ -970,7 +974,7 @@ export const PANEL_CATEGORY_MAP: Record<string, { labelKey: string; panelKeys: s
}, },
commodityEcon: { commodityEcon: {
labelKey: 'header.panelCatCommodityEcon', labelKey: 'header.panelCatCommodityEcon',
panelKeys: ['trade-policy', 'economic', 'gulf-economies', 'gcc-investments', 'finance', 'polymarket', 'airline-intel', 'world-clock', 'monitors'], panelKeys: ['trade-policy', 'sanctions-pressure', 'economic', 'gulf-economies', 'gcc-investments', 'finance', 'polymarket', 'airline-intel', 'world-clock', 'monitors'],
variants: ['commodity'], variants: ['commodity'],
}, },

View File

@@ -0,0 +1,141 @@
// @ts-nocheck
// Code generated by protoc-gen-ts-client. DO NOT EDIT.
// source: worldmonitor/sanctions/v1/service.proto
export interface ListSanctionsPressureRequest {
maxItems: number;
}
export interface ListSanctionsPressureResponse {
entries: SanctionsEntry[];
countries: CountrySanctionsPressure[];
programs: ProgramSanctionsPressure[];
fetchedAt: string;
datasetDate: string;
totalCount: number;
sdnCount: number;
consolidatedCount: number;
newEntryCount: number;
vesselCount: number;
aircraftCount: number;
}
export interface SanctionsEntry {
id: string;
name: string;
entityType: SanctionsEntityType;
countryCodes: string[];
countryNames: string[];
programs: string[];
sourceLists: string[];
effectiveAt: string;
isNew: boolean;
note: string;
}
export interface CountrySanctionsPressure {
countryCode: string;
countryName: string;
entryCount: number;
newEntryCount: number;
vesselCount: number;
aircraftCount: number;
}
export interface ProgramSanctionsPressure {
program: string;
entryCount: number;
newEntryCount: number;
}
export type SanctionsEntityType = "SANCTIONS_ENTITY_TYPE_UNSPECIFIED" | "SANCTIONS_ENTITY_TYPE_ENTITY" | "SANCTIONS_ENTITY_TYPE_INDIVIDUAL" | "SANCTIONS_ENTITY_TYPE_VESSEL" | "SANCTIONS_ENTITY_TYPE_AIRCRAFT";
export interface FieldViolation {
field: string;
description: string;
}
export class ValidationError extends Error {
violations: FieldViolation[];
constructor(violations: FieldViolation[]) {
super("Validation failed");
this.name = "ValidationError";
this.violations = violations;
}
}
export class ApiError extends Error {
statusCode: number;
body: string;
constructor(statusCode: number, message: string, body: string) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
this.body = body;
}
}
export interface SanctionsServiceClientOptions {
fetch?: typeof fetch;
defaultHeaders?: Record<string, string>;
}
export interface SanctionsServiceCallOptions {
headers?: Record<string, string>;
signal?: AbortSignal;
}
export class SanctionsServiceClient {
private baseURL: string;
private fetchFn: typeof fetch;
private defaultHeaders: Record<string, string>;
constructor(baseURL: string, options?: SanctionsServiceClientOptions) {
this.baseURL = baseURL.replace(/\/+$/, "");
this.fetchFn = options?.fetch ?? globalThis.fetch;
this.defaultHeaders = { ...options?.defaultHeaders };
}
async listSanctionsPressure(req: ListSanctionsPressureRequest, options?: SanctionsServiceCallOptions): Promise<ListSanctionsPressureResponse> {
let path = "/api/sanctions/v1/list-sanctions-pressure";
const params = new URLSearchParams();
if (req.maxItems != null && req.maxItems !== 0) params.set("max_items", String(req.maxItems));
const url = this.baseURL + path + (params.toString() ? "?" + params.toString() : "");
const headers: Record<string, string> = {
"Content-Type": "application/json",
...this.defaultHeaders,
...options?.headers,
};
const resp = await this.fetchFn(url, {
method: "GET",
headers,
signal: options?.signal,
});
if (!resp.ok) {
return this.handleError(resp);
}
return await resp.json() as ListSanctionsPressureResponse;
}
private async handleError(resp: Response): Promise<never> {
const body = await resp.text();
if (resp.status === 400) {
try {
const parsed = JSON.parse(body);
if (parsed.violations) {
throw new ValidationError(parsed.violations);
}
} catch (e) {
if (e instanceof ValidationError) throw e;
}
}
throw new ApiError(resp.status, `Request failed with status ${resp.status}`, body);
}
}

View File

@@ -0,0 +1,155 @@
// @ts-nocheck
// Code generated by protoc-gen-ts-server. DO NOT EDIT.
// source: worldmonitor/sanctions/v1/service.proto
export interface ListSanctionsPressureRequest {
maxItems: number;
}
export interface ListSanctionsPressureResponse {
entries: SanctionsEntry[];
countries: CountrySanctionsPressure[];
programs: ProgramSanctionsPressure[];
fetchedAt: string;
datasetDate: string;
totalCount: number;
sdnCount: number;
consolidatedCount: number;
newEntryCount: number;
vesselCount: number;
aircraftCount: number;
}
export interface SanctionsEntry {
id: string;
name: string;
entityType: SanctionsEntityType;
countryCodes: string[];
countryNames: string[];
programs: string[];
sourceLists: string[];
effectiveAt: string;
isNew: boolean;
note: string;
}
export interface CountrySanctionsPressure {
countryCode: string;
countryName: string;
entryCount: number;
newEntryCount: number;
vesselCount: number;
aircraftCount: number;
}
export interface ProgramSanctionsPressure {
program: string;
entryCount: number;
newEntryCount: number;
}
export type SanctionsEntityType = "SANCTIONS_ENTITY_TYPE_UNSPECIFIED" | "SANCTIONS_ENTITY_TYPE_ENTITY" | "SANCTIONS_ENTITY_TYPE_INDIVIDUAL" | "SANCTIONS_ENTITY_TYPE_VESSEL" | "SANCTIONS_ENTITY_TYPE_AIRCRAFT";
export interface FieldViolation {
field: string;
description: string;
}
export class ValidationError extends Error {
violations: FieldViolation[];
constructor(violations: FieldViolation[]) {
super("Validation failed");
this.name = "ValidationError";
this.violations = violations;
}
}
export class ApiError extends Error {
statusCode: number;
body: string;
constructor(statusCode: number, message: string, body: string) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
this.body = body;
}
}
export interface ServerContext {
request: Request;
pathParams: Record<string, string>;
headers: Record<string, string>;
}
export interface ServerOptions {
onError?: (error: unknown, req: Request) => Response | Promise<Response>;
validateRequest?: (methodName: string, body: unknown) => FieldViolation[] | undefined;
}
export interface RouteDescriptor {
method: string;
path: string;
handler: (req: Request) => Promise<Response>;
}
export interface SanctionsServiceHandler {
listSanctionsPressure(ctx: ServerContext, req: ListSanctionsPressureRequest): Promise<ListSanctionsPressureResponse>;
}
export function createSanctionsServiceRoutes(
handler: SanctionsServiceHandler,
options?: ServerOptions,
): RouteDescriptor[] {
return [
{
method: "GET",
path: "/api/sanctions/v1/list-sanctions-pressure",
handler: async (req: Request): Promise<Response> => {
try {
const pathParams: Record<string, string> = {};
const url = new URL(req.url, "http://localhost");
const params = url.searchParams;
const body: ListSanctionsPressureRequest = {
maxItems: Number(params.get("max_items") ?? "0"),
};
if (options?.validateRequest) {
const bodyViolations = options.validateRequest("listSanctionsPressure", body);
if (bodyViolations) {
throw new ValidationError(bodyViolations);
}
}
const ctx: ServerContext = {
request: req,
pathParams,
headers: Object.fromEntries(req.headers.entries()),
};
const result = await handler.listSanctionsPressure(ctx, body);
return new Response(JSON.stringify(result as ListSanctionsPressureResponse), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (err: unknown) {
if (err instanceof ValidationError) {
return new Response(JSON.stringify({ violations: err.violations }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (options?.onError) {
return options.onError(err, req);
}
const message = err instanceof Error ? err.message : String(err);
return new Response(JSON.stringify({ message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
},
},
];
}

View File

@@ -1,5 +1,6 @@
import { getLocationName, type GeoConvergenceAlert } from './geo-convergence'; import { getLocationName, type GeoConvergenceAlert } from './geo-convergence';
import type { CountryScore } from './country-instability'; import type { CountryScore } from './country-instability';
import { getLatestSanctionsPressure, type SanctionsPressureResult } from './sanctions-pressure';
import { getLatestRadiationWatch, type RadiationObservation } from './radiation'; import { getLatestRadiationWatch, type RadiationObservation } from './radiation';
import type { CascadeResult, CascadeImpactLevel } from '@/types'; import type { CascadeResult, CascadeImpactLevel } from '@/types';
import { calculateCII, isInLearningMode } from './country-instability'; import { calculateCII, isInLearningMode } from './country-instability';
@@ -8,7 +9,7 @@ import { t } from '@/services/i18n';
import type { TheaterPostureSummary } from '@/services/military-surge'; import type { TheaterPostureSummary } from '@/services/military-surge';
export type AlertPriority = 'critical' | 'high' | 'medium' | 'low'; export type AlertPriority = 'critical' | 'high' | 'medium' | 'low';
export type AlertType = 'convergence' | 'cii_spike' | 'cascade' | 'radiation' | 'composite'; export type AlertType = 'convergence' | 'cii_spike' | 'cascade' | 'sanctions' | 'radiation' | 'composite';
export interface UnifiedAlert { export interface UnifiedAlert {
id: string; id: string;
@@ -20,6 +21,7 @@ export interface UnifiedAlert {
convergence?: GeoConvergenceAlert; convergence?: GeoConvergenceAlert;
ciiChange?: CIIChangeAlert; ciiChange?: CIIChangeAlert;
cascade?: CascadeAlert; cascade?: CascadeAlert;
sanctions?: SanctionsAlert;
radiation?: RadiationAlert; radiation?: RadiationAlert;
}; };
location?: { lat: number; lon: number }; location?: { lat: number; lon: number };
@@ -45,6 +47,20 @@ export interface CascadeAlert {
highestImpact: CascadeImpactLevel; highestImpact: CascadeImpactLevel;
} }
export interface SanctionsAlert {
countryCode: string;
countryName: string;
entryCount: number;
newEntryCount: number;
topProgram: string;
topProgramCount: number;
vesselCount: number;
aircraftCount: number;
totalCount: number;
datasetDate: number | null;
}
export interface RadiationAlert { export interface RadiationAlert {
siteId: string; siteId: string;
siteName: string; siteName: string;
@@ -134,6 +150,15 @@ function getPriorityFromConvergence(score: number, typeCount: number): AlertPrio
return 'low'; return 'low';
} }
function getPriorityFromSanctions(data: SanctionsPressureResult): AlertPriority {
const leadEntryCount = data.countries[0]?.entryCount ?? 0;
if (data.newEntryCount >= 10) return 'critical';
if (data.newEntryCount >= 3 || leadEntryCount >= 60) return 'high';
if (data.newEntryCount >= 1 || leadEntryCount >= 25) return 'medium';
return 'low';
}
function getPriorityFromRadiation(observation: RadiationObservation, spikeCount: number): AlertPriority { function getPriorityFromRadiation(observation: RadiationObservation, spikeCount: number): AlertPriority {
let score = 0; let score = 0;
if (observation.severity === 'spike') score += 4; if (observation.severity === 'spike') score += 4;
@@ -238,6 +263,54 @@ export function createCascadeAlert(cascade: CascadeResult): UnifiedAlert | null
return addAndMergeAlert(alert); return addAndMergeAlert(alert);
} }
function createSanctionsAlert(): UnifiedAlert | null {
const pressure = getLatestSanctionsPressure();
if (!pressure || pressure.totalCount === 0) {
for (let i = alerts.length - 1; i >= 0; i--) {
if (alerts[i]?.type === 'sanctions') alerts.splice(i, 1);
}
return null;
}
const leadCountry = [...pressure.countries]
.sort((a, b) => b.newEntryCount - a.newEntryCount || b.entryCount - a.entryCount)[0];
if (!leadCountry) return null;
if (pressure.newEntryCount === 0 && leadCountry.entryCount < 25) return null;
const leadProgram = [...pressure.programs]
.sort((a, b) => b.newEntryCount - a.newEntryCount || b.entryCount - a.entryCount)[0];
const sanctions: SanctionsAlert = {
countryCode: leadCountry.countryCode,
countryName: leadCountry.countryName,
entryCount: leadCountry.entryCount,
newEntryCount: leadCountry.newEntryCount,
topProgram: leadProgram?.program || 'Unspecified',
topProgramCount: leadProgram?.entryCount || 0,
vesselCount: leadCountry.vesselCount,
aircraftCount: leadCountry.aircraftCount,
totalCount: pressure.totalCount,
datasetDate: pressure.datasetDate?.getTime() ?? null,
};
const summary = pressure.newEntryCount > 0
? `${pressure.newEntryCount} new OFAC designation${pressure.newEntryCount === 1 ? '' : 's'} detected. Pressure is highest around ${leadCountry.countryName} (${leadCountry.entryCount}), with ${leadProgram?.program || 'unspecified'} leading program activity.`
: `${leadCountry.countryName} has ${leadCountry.entryCount} OFAC-linked designations in the current dataset, led by ${leadProgram?.program || 'unspecified'} activity.`;
return addAndMergeAlert({
id: 'sanctions-pressure',
type: 'sanctions',
priority: getPriorityFromSanctions(pressure),
title: pressure.newEntryCount > 0
? `Sanctions pressure rising around ${leadCountry.countryName}`
: `Persistent sanctions pressure around ${leadCountry.countryName}`,
summary,
components: { sanctions },
countries: [leadCountry.countryCode],
timestamp: pressure.fetchedAt,
});
}
function getRadiationRank(observation: RadiationObservation): number { function getRadiationRank(observation: RadiationObservation): number {
const severityRank = observation.severity === 'spike' ? 2 : observation.severity === 'elevated' ? 1 : 0; const severityRank = observation.severity === 'spike' ? 2 : observation.severity === 'elevated' ? 1 : 0;
const confidenceRank = observation.confidence === 'high' ? 2 : observation.confidence === 'medium' ? 1 : 0; const confidenceRank = observation.confidence === 'high' ? 2 : observation.confidence === 'medium' ? 1 : 0;
@@ -367,7 +440,12 @@ function generateCompositeTitle(a: UnifiedAlert, b: UnifiedAlert): string {
} }
if (a.components.convergence || b.components.convergence) { if (a.components.convergence || b.components.convergence) {
const countryCode = a.countries[0] || b.countries[0]; if (a.components.sanctions || b.components.sanctions) {
const sanctions = a.components.sanctions || b.components.sanctions;
if (sanctions) return `Sanctions pressure: ${sanctions.countryName}`;
}
const countryCode = a.countries[0] || b.countries[0];
const location = countryCode ? getCountryDisplayName(countryCode) : t('alerts.multipleRegions'); const location = countryCode ? getCountryDisplayName(countryCode) : t('alerts.multipleRegions');
return t('alerts.geoAlert', { location }); return t('alerts.geoAlert', { location });
} }
@@ -376,6 +454,11 @@ function generateCompositeTitle(a: UnifiedAlert, b: UnifiedAlert): string {
return t('alerts.cascadeAlert'); return t('alerts.cascadeAlert');
} }
if (a.components.sanctions || b.components.sanctions) {
const sanctions = a.components.sanctions || b.components.sanctions;
if (sanctions) return `Sanctions pressure: ${sanctions.countryName}`;
}
const countryCode = a.countries[0] || b.countries[0]; const countryCode = a.countries[0] || b.countries[0];
const location = countryCode ? getCountryDisplayName(countryCode) : t('alerts.multipleRegions'); const location = countryCode ? getCountryDisplayName(countryCode) : t('alerts.multipleRegions');
return t('alerts.alert', { location }); return t('alerts.alert', { location });
@@ -528,6 +611,7 @@ function updateAlerts(convergenceAlerts: GeoConvergenceAlert[]): void {
// Check for CII changes (alerts are added internally via addAndMergeAlert) // Check for CII changes (alerts are added internally via addAndMergeAlert)
checkCIIChanges(); checkCIIChanges();
createSanctionsAlert();
createRadiationAlert(); createRadiationAlert();
// Sort by timestamp (newest first) and limit to 100 // Sort by timestamp (newest first) and limit to 100
@@ -549,6 +633,18 @@ export function calculateStrategicRiskOverview(
updateAlerts(convergenceAlerts); updateAlerts(convergenceAlerts);
const ciiRiskScore = calculateCIIRiskScore(ciiScores); const ciiRiskScore = calculateCIIRiskScore(ciiScores);
const sanctionsPressure = getLatestSanctionsPressure();
const sanctionsScore = sanctionsPressure
? Math.min(
10,
sanctionsPressure.newEntryCount * 2 +
Math.min(4, (sanctionsPressure.countries[0]?.entryCount ?? 0) / 20) +
sanctionsPressure.vesselCount * 0.3 +
sanctionsPressure.aircraftCount * 0.3
)
: 0;
const radiationWatch = getLatestRadiationWatch(); const radiationWatch = getLatestRadiationWatch();
const radiationScore = radiationWatch const radiationScore = radiationWatch
? Math.min( ? Math.min(
@@ -590,6 +686,7 @@ export function calculateStrategicRiskOverview(
infraScore * infraWeight + infraScore * infraWeight +
theaterBoost + theaterBoost +
breakingBoost + breakingBoost +
sanctionsScore +
radiationScore radiationScore
)); ));
@@ -605,7 +702,7 @@ export function calculateStrategicRiskOverview(
infrastructureIncidents: countInfrastructureIncidents(), infrastructureIncidents: countInfrastructureIncidents(),
compositeScore: composite, compositeScore: composite,
trend, trend,
topRisks: identifyTopRisks(convergenceAlerts, ciiScores, radiationWatch?.observations ?? []), topRisks: identifyTopRisks(convergenceAlerts, ciiScores, sanctionsPressure, radiationWatch?.observations ?? []),
topConvergenceZones: convergenceAlerts topConvergenceZones: convergenceAlerts
.slice(0, 3) .slice(0, 3)
.map(a => ({ cellId: a.cellId, lat: a.lat, lon: a.lon, score: a.score })), .map(a => ({ cellId: a.cellId, lat: a.lat, lon: a.lon, score: a.score })),
@@ -662,6 +759,7 @@ function countInfrastructureIncidents(): number {
function identifyTopRisks( function identifyTopRisks(
convergence: GeoConvergenceAlert[], convergence: GeoConvergenceAlert[],
cii: CountryScore[], cii: CountryScore[],
sanctions: SanctionsPressureResult | null,
radiation: RadiationObservation[] radiation: RadiationObservation[]
): string[] { ): string[] {
const risks: string[] = []; const risks: string[] = [];
@@ -672,6 +770,12 @@ function identifyTopRisks(
risks.push(`Convergence: ${location} (score: ${top.score})`); risks.push(`Convergence: ${location} (score: ${top.score})`);
} }
const leadSanctions = sanctions?.countries[0];
if (leadSanctions && (sanctions.newEntryCount > 0 || leadSanctions.entryCount >= 25)) {
const label = sanctions.newEntryCount > 0 ? 'Sanctions burst' : 'Sanctions pressure';
risks.push(`${label}: ${leadSanctions.countryName} (${leadSanctions.entryCount}, +${leadSanctions.newEntryCount} new)`);
}
const strongestRadiation = radiation const strongestRadiation = radiation
.filter(observation => observation.severity !== 'normal') .filter(observation => observation.severity !== 'normal')
.sort((a, b) => getRadiationRank(b) - getRadiationRank(a))[0]; .sort((a, b) => getRadiationRank(b) - getRadiationRank(a))[0];

View File

@@ -74,6 +74,7 @@ const SOURCE_METADATA: Record<DataSourceId, { name: string; requiredForRisk: boo
wto_trade: { name: 'WTO Trade Policy', requiredForRisk: false, panelId: 'trade-policy' }, wto_trade: { name: 'WTO Trade Policy', requiredForRisk: false, panelId: 'trade-policy' },
supply_chain: { name: 'Supply Chain Intelligence', requiredForRisk: false, panelId: 'supply-chain' }, supply_chain: { name: 'Supply Chain Intelligence', requiredForRisk: false, panelId: 'supply-chain' },
security_advisories: { name: 'Security Advisories', requiredForRisk: false, panelId: 'security-advisories' }, security_advisories: { name: 'Security Advisories', requiredForRisk: false, panelId: 'security-advisories' },
sanctions_pressure: { name: 'Sanctions Pressure', requiredForRisk: false, panelId: 'sanctions-pressure' },
radiation: { name: 'Radiation Watch', requiredForRisk: false, panelId: 'radiation-watch' }, radiation: { name: 'Radiation Watch', requiredForRisk: false, panelId: 'radiation-watch' },
gpsjam: { name: 'GPS/GNSS Interference', requiredForRisk: false, panelId: 'map' }, gpsjam: { name: 'GPS/GNSS Interference', requiredForRisk: false, panelId: 'map' },
treasury_revenue: { name: 'Treasury Customs Revenue', requiredForRisk: false, panelId: 'trade-policy' }, treasury_revenue: { name: 'Treasury Customs Revenue', requiredForRisk: false, panelId: 'trade-policy' },
@@ -336,6 +337,7 @@ const INTELLIGENCE_GAP_MESSAGES: Record<DataSourceId, string> = {
wto_trade: 'Trade policy intelligence unavailable—WTO data not updating', wto_trade: 'Trade policy intelligence unavailable—WTO data not updating',
supply_chain: 'Supply chain disruption status unavailable—chokepoint monitoring offline', supply_chain: 'Supply chain disruption status unavailable—chokepoint monitoring offline',
security_advisories: 'Government travel advisory data unavailable—security alerts may be missed', security_advisories: 'Government travel advisory data unavailable—security alerts may be missed',
sanctions_pressure: 'Structured sanctions pressure unavailable\u2014OFAC designation visibility reduced',
radiation: 'Radiation monitoring degraded—EPA RadNet and Safecast observations unavailable', radiation: 'Radiation monitoring degraded—EPA RadNet and Safecast observations unavailable',
gpsjam: 'GPS/GNSS interference data unavailable—jamming zones undetected', gpsjam: 'GPS/GNSS interference data unavailable—jamming zones undetected',
treasury_revenue: 'US Treasury customs revenue data unavailable', treasury_revenue: 'US Treasury customs revenue data unavailable',

View File

@@ -22,6 +22,7 @@ const SIGNAL_TYPE_LABELS: Record<SignalType, string> = {
satellite_fire: 'satellite fires', satellite_fire: 'satellite fires',
radiation_anomaly: 'radiation anomalies', radiation_anomaly: 'radiation anomalies',
temporal_anomaly: 'anomaly detection', temporal_anomaly: 'anomaly detection',
sanctions_pressure: 'sanctions pressure',
active_strike: 'active strikes', active_strike: 'active strikes',
}; };
@@ -34,6 +35,7 @@ const SIGNAL_TYPE_ICONS: Record<SignalType, string> = {
satellite_fire: '🔥', satellite_fire: '🔥',
radiation_anomaly: '☢️', radiation_anomaly: '☢️',
temporal_anomaly: '📊', temporal_anomaly: '📊',
sanctions_pressure: '🚫',
active_strike: '💥', active_strike: '💥',
}; };
@@ -291,6 +293,7 @@ class FocalPointDetector {
(signals.signalTypes.has('military_vessel') && /navy|naval|ships|fleet|carrier/.test(lower)) || (signals.signalTypes.has('military_vessel') && /navy|naval|ships|fleet|carrier/.test(lower)) ||
(signals.signalTypes.has('protest') && /protest|demonstrat|unrest|riot/.test(lower)) || (signals.signalTypes.has('protest') && /protest|demonstrat|unrest|riot/.test(lower)) ||
(signals.signalTypes.has('internet_outage') && /internet|blackout|outage|connectivity/.test(lower)) || (signals.signalTypes.has('internet_outage') && /internet|blackout|outage|connectivity/.test(lower)) ||
(signals.signalTypes.has('sanctions_pressure') && /sanction|designation|ofac|treasury|embargo|blacklist/.test(lower)) ||
(signals.signalTypes.has('radiation_anomaly') && /nuclear|radiation|reactor|contamination|radnet/.test(lower)) || (signals.signalTypes.has('radiation_anomaly') && /nuclear|radiation|reactor|contamination|radnet/.test(lower)) ||
(signals.signalTypes.has('active_strike') && /strike|attack|bomb|missile|target|hit/.test(lower)); (signals.signalTypes.has('active_strike') && /strike|attack|bomb|missile|target|hit/.test(lower));
})) { })) {

View File

@@ -41,6 +41,8 @@ export * from './trade';
export * from './supply-chain'; export * from './supply-chain';
export * from './radiation'; export * from './radiation';
export * from './breaking-news-alerts'; export * from './breaking-news-alerts';
export * from './sanctions-pressure';
export * from './sanctions-pressure';
export * from './daily-market-brief'; export * from './daily-market-brief';
export * from './stock-analysis-history'; export * from './stock-analysis-history';
export * from './stock-backtest'; export * from './stock-backtest';

View File

@@ -0,0 +1,172 @@
import { createCircuitBreaker } from '@/utils';
import { getRpcBaseUrl } from '@/services/rpc-client';
import { getHydratedData } from '@/services/bootstrap';
import {
SanctionsServiceClient,
type SanctionsEntry as ProtoSanctionsEntry,
type SanctionsEntityType as ProtoSanctionsEntityType,
type CountrySanctionsPressure as ProtoCountryPressure,
type ProgramSanctionsPressure as ProtoProgramPressure,
type ListSanctionsPressureResponse,
} from '@/generated/client/worldmonitor/sanctions/v1/service_client';
export type SanctionsEntityType = 'entity' | 'individual' | 'vessel' | 'aircraft';
export interface SanctionsEntry {
id: string;
name: string;
entityType: SanctionsEntityType;
countryCodes: string[];
countryNames: string[];
programs: string[];
sourceLists: string[];
effectiveAt: Date | null;
isNew: boolean;
note: string;
}
export interface CountrySanctionsPressure {
countryCode: string;
countryName: string;
entryCount: number;
newEntryCount: number;
vesselCount: number;
aircraftCount: number;
}
export interface ProgramSanctionsPressure {
program: string;
entryCount: number;
newEntryCount: number;
}
export interface SanctionsPressureResult {
fetchedAt: Date;
datasetDate: Date | null;
totalCount: number;
sdnCount: number;
consolidatedCount: number;
newEntryCount: number;
vesselCount: number;
aircraftCount: number;
countries: CountrySanctionsPressure[];
programs: ProgramSanctionsPressure[];
entries: SanctionsEntry[];
}
const client = new SanctionsServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args) });
const breaker = createCircuitBreaker<SanctionsPressureResult>({
name: 'Sanctions Pressure',
cacheTtlMs: 30 * 60 * 1000,
persistCache: true,
});
let latestSanctionsPressureResult: SanctionsPressureResult | null = null;
const emptyResult: SanctionsPressureResult = {
fetchedAt: new Date(0),
datasetDate: null,
totalCount: 0,
sdnCount: 0,
consolidatedCount: 0,
newEntryCount: 0,
vesselCount: 0,
aircraftCount: 0,
countries: [],
programs: [],
entries: [],
};
function mapEntityType(value: ProtoSanctionsEntityType): SanctionsEntityType {
switch (value) {
case 'SANCTIONS_ENTITY_TYPE_INDIVIDUAL':
return 'individual';
case 'SANCTIONS_ENTITY_TYPE_VESSEL':
return 'vessel';
case 'SANCTIONS_ENTITY_TYPE_AIRCRAFT':
return 'aircraft';
default:
return 'entity';
}
}
function parseEpoch(value: string | number | null | undefined): Date | null {
if (value == null) return null;
const asNumber = typeof value === 'number' ? value : Number(value);
if (!Number.isFinite(asNumber) || asNumber <= 0) return null;
return new Date(asNumber);
}
function toEntry(raw: ProtoSanctionsEntry): SanctionsEntry {
return {
id: raw.id,
name: raw.name,
entityType: mapEntityType(raw.entityType),
countryCodes: raw.countryCodes ?? [],
countryNames: raw.countryNames ?? [],
programs: raw.programs ?? [],
sourceLists: raw.sourceLists ?? [],
effectiveAt: parseEpoch(raw.effectiveAt as string | number | undefined),
isNew: raw.isNew ?? false,
note: raw.note ?? '',
};
}
function toCountry(raw: ProtoCountryPressure): CountrySanctionsPressure {
return {
countryCode: raw.countryCode,
countryName: raw.countryName,
entryCount: raw.entryCount ?? 0,
newEntryCount: raw.newEntryCount ?? 0,
vesselCount: raw.vesselCount ?? 0,
aircraftCount: raw.aircraftCount ?? 0,
};
}
function toProgram(raw: ProtoProgramPressure): ProgramSanctionsPressure {
return {
program: raw.program,
entryCount: raw.entryCount ?? 0,
newEntryCount: raw.newEntryCount ?? 0,
};
}
function toResult(response: ListSanctionsPressureResponse): SanctionsPressureResult {
return {
fetchedAt: parseEpoch(response.fetchedAt as string | number | undefined) || new Date(),
datasetDate: parseEpoch(response.datasetDate as string | number | undefined),
totalCount: response.totalCount ?? 0,
sdnCount: response.sdnCount ?? 0,
consolidatedCount: response.consolidatedCount ?? 0,
newEntryCount: response.newEntryCount ?? 0,
vesselCount: response.vesselCount ?? 0,
aircraftCount: response.aircraftCount ?? 0,
countries: (response.countries ?? []).map(toCountry),
programs: (response.programs ?? []).map(toProgram),
entries: (response.entries ?? []).map(toEntry),
};
}
export async function fetchSanctionsPressure(): Promise<SanctionsPressureResult> {
const hydrated = getHydratedData('sanctionsPressure') as ListSanctionsPressureResponse | undefined;
if (hydrated?.entries?.length || hydrated?.countries?.length || hydrated?.programs?.length) {
const result = toResult(hydrated);
latestSanctionsPressureResult = result;
return result;
}
return breaker.execute(async () => {
const response = await client.listSanctionsPressure({
maxItems: 30,
}, {
signal: AbortSignal.timeout(25_000),
});
const result = toResult(response);
latestSanctionsPressureResult = result;
return result;
}, emptyResult);
}
export function getLatestSanctionsPressure(): SanctionsPressureResult | null {
return latestSanctionsPressureResult;
}

View File

@@ -11,6 +11,7 @@ import type {
SocialUnrestEvent, SocialUnrestEvent,
AisDisruptionEvent, AisDisruptionEvent,
} from '@/types'; } from '@/types';
import type { CountrySanctionsPressure } from './sanctions-pressure';
import type { RadiationObservation } from './radiation'; import type { RadiationObservation } from './radiation';
import { getCountryAtCoordinates, getCountryNameByCode, nameToCountryCode, ME_STRIKE_BOUNDS, resolveCountryFromBounds } from './country-geometry'; import { getCountryAtCoordinates, getCountryNameByCode, nameToCountryCode, ME_STRIKE_BOUNDS, resolveCountryFromBounds } from './country-geometry';
@@ -22,7 +23,8 @@ export type SignalType =
| 'ais_disruption' | 'ais_disruption'
| 'satellite_fire' // NASA FIRMS thermal anomalies | 'satellite_fire' // NASA FIRMS thermal anomalies
| 'radiation_anomaly' // Radiation readings meaningfully above local baseline | 'radiation_anomaly' // Radiation readings meaningfully above local baseline
| 'temporal_anomaly' // Baseline deviation alerts | 'temporal_anomaly'
| 'sanctions_pressure' // Baseline deviation alerts
| 'active_strike' // Iran attack / military conflict events | 'active_strike' // Iran attack / military conflict events
export interface GeoSignal { export interface GeoSignal {
@@ -328,6 +330,36 @@ class SignalAggregator {
this.pruneOld(); this.pruneOld();
} }
ingestSanctionsPressure(countries: CountrySanctionsPressure[]): void {
this.clearSignalType('sanctions_pressure');
for (const country of countries) {
const code = normalizeCountryCode(country.countryCode || country.countryName);
const severity: 'low' | 'medium' | 'high' =
country.newEntryCount >= 5 || country.entryCount >= 50
? 'high'
: country.newEntryCount >= 1 || country.entryCount >= 20
? 'medium'
: 'low';
if (country.newEntryCount === 0 && country.entryCount < 20) continue;
this.signals.push({
type: 'sanctions_pressure',
country: code,
countryName: country.countryName || getCountryName(code),
lat: 0,
lon: 0,
severity,
title: country.newEntryCount > 0
? `${country.newEntryCount} new OFAC designation${country.newEntryCount === 1 ? '' : 's'} tied to ${country.countryName}`
: `${country.entryCount} OFAC-linked designations tied to ${country.countryName}`,
timestamp: new Date(),
});
}
this.pruneOld();
}
ingestConflictEvents(events: Array<{ ingestConflictEvents(events: Array<{
id: string; id: string;
category: string; category: string;
@@ -504,6 +536,7 @@ class SignalAggregator {
satellite_fire: 'thermal anomalies', satellite_fire: 'thermal anomalies',
radiation_anomaly: 'radiation anomalies', radiation_anomaly: 'radiation anomalies',
temporal_anomaly: 'baseline anomalies', temporal_anomaly: 'baseline anomalies',
sanctions_pressure: 'sanctions pressure',
active_strike: 'active strikes', active_strike: 'active strikes',
}; };
@@ -561,6 +594,7 @@ class SignalAggregator {
satellite_fire: 0, satellite_fire: 0,
radiation_anomaly: 0, radiation_anomaly: 0,
temporal_anomaly: 0, temporal_anomaly: 0,
sanctions_pressure: 0,
active_strike: 0, active_strike: 0,
}; };

View File

@@ -8554,6 +8554,20 @@ a.prediction-link:hover {
text-align: right; text-align: right;
} }
.sanctions-panel-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.sanctions-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
gap: 8px;
padding: 0 8px;
}
.radiation-panel-content { .radiation-panel-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -8567,6 +8581,15 @@ a.prediction-link:hover {
padding: 0 8px; padding: 0 8px;
} }
.sanctions-summary-card {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px;
border: 1px solid var(--border);
background: var(--overlay-subtle);
}
.radiation-summary-card { .radiation-summary-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -8576,6 +8599,11 @@ a.prediction-link:hover {
background: var(--overlay-subtle); background: var(--overlay-subtle);
} }
.sanctions-summary-card-highlight {
border-color: rgba(245, 158, 11, 0.35);
background: rgba(245, 158, 11, 0.08);
}
.radiation-summary-card-spike { .radiation-summary-card-spike {
border-color: rgba(239, 68, 68, 0.35); border-color: rgba(239, 68, 68, 0.35);
background: rgba(239, 68, 68, 0.08); background: rgba(239, 68, 68, 0.08);
@@ -8591,11 +8619,25 @@ a.prediction-link:hover {
background: rgba(245, 158, 11, 0.08); background: rgba(245, 158, 11, 0.08);
} }
.sanctions-summary-card-muted {
border-color: rgba(125, 211, 252, 0.35);
background: rgba(125, 211, 252, 0.08);
}
.radiation-summary-card-conflict { .radiation-summary-card-conflict {
border-color: rgba(125, 211, 252, 0.35); border-color: rgba(125, 211, 252, 0.35);
background: rgba(125, 211, 252, 0.08); background: rgba(125, 211, 252, 0.08);
} }
.sanctions-summary-label,
.sanctions-headline-label,
.sanctions-section-title {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-dim);
}
.radiation-summary-label { .radiation-summary-label {
font-size: 9px; font-size: 9px;
text-transform: uppercase; text-transform: uppercase;
@@ -8603,12 +8645,79 @@ a.prediction-link:hover {
color: var(--text-dim); color: var(--text-dim);
} }
.sanctions-summary-value {
font-size: 18px;
font-weight: 600;
color: var(--accent);
}
.radiation-summary-value { .radiation-summary-value {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--accent); color: var(--accent);
} }
.sanctions-headlines,
.sanctions-sections {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 8px;
}
.sanctions-headline,
.sanctions-section {
border: 1px solid var(--border);
background: var(--surface);
}
.sanctions-headline {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
align-items: baseline;
}
.sanctions-headline-value {
font-size: 12px;
color: var(--text-secondary);
text-align: right;
}
.sanctions-section-title {
padding: 8px 10px 0;
}
.sanctions-list {
display: flex;
flex-direction: column;
}
.sanctions-row,
.sanctions-entry {
padding: 8px 10px;
border-top: 1px solid var(--border);
}
.sanctions-row {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
}
.sanctions-row-main {
min-width: 0;
}
.sanctions-row-title,
.sanctions-entry-name {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
}
.radiation-table { .radiation-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -8641,12 +8750,45 @@ a.prediction-link:hover {
color: var(--text-secondary); color: var(--text-secondary);
} }
.sanctions-row-meta,
.sanctions-entry-meta,
.sanctions-entry-note {
margin-top: 2px;
font-size: 10px;
color: var(--text-dim);
}
.radiation-location-meta { .radiation-location-meta {
margin-top: 2px; margin-top: 2px;
font-size: 10px; font-size: 10px;
color: var(--text-dim); color: var(--text-dim);
} }
.sanctions-row-flags,
.sanctions-entry-top {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.sanctions-pill {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 999px;
border: 1px solid var(--border);
font-size: 10px;
color: var(--text-secondary);
background: var(--overlay-subtle);
}
.sanctions-pill-new {
color: var(--semantic-elevated);
border-color: rgba(245, 158, 11, 0.35);
background: rgba(245, 158, 11, 0.08);
}
.radiation-location-flags { .radiation-location-flags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -8681,6 +8823,10 @@ a.prediction-link:hover {
background: rgba(245, 158, 11, 0.08); background: rgba(245, 158, 11, 0.08);
} }
.sanctions-pill-type {
text-transform: uppercase;
}
.radiation-confidence-low { .radiation-confidence-low {
color: #7dd3fc; color: #7dd3fc;
border-color: rgba(125, 211, 252, 0.35); border-color: rgba(125, 211, 252, 0.35);

View File

@@ -30,6 +30,7 @@ export type DataSourceId =
| 'supply_chain' | 'supply_chain'
| 'security_advisories' | 'security_advisories'
| 'gpsjam' | 'gpsjam'
| 'sanctions_pressure'
| 'radiation' | 'radiation'
| 'treasury_revenue'; | 'treasury_revenue';

View File

@@ -0,0 +1,148 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const handlerSrc = readFileSync('server/worldmonitor/sanctions/v1/list-sanctions-pressure.ts', 'utf8');
const seedSrc = readFileSync('scripts/seed-sanctions-pressure.mjs', 'utf8');
// ---------------------------------------------------------------------------
// P2-1: _state must not leak through trimResponse
// ---------------------------------------------------------------------------
describe('trimResponse: _state stripping', () => {
it('handler trimResponse destructures _state before spreading data', () => {
assert.match(
handlerSrc,
/_state.*_discarded.*\.\.\.rest/s,
'trimResponse must destructure _state out before spreading to prevent leaking seed internals to API clients',
);
});
it('seed does not embed _state in the canonical Redis payload directly', () => {
// The canonical payload must go through extraKeys or afterPublish, not inline
const fetchFnStart = seedSrc.indexOf('async function fetchSanctionsPressure()');
const fetchFnEnd = seedSrc.indexOf('\nfunction validate(');
const fetchFnBody = seedSrc.slice(fetchFnStart, fetchFnEnd);
// _state must only appear as a separate top-level key, not inside entries/countries/programs
assert.match(
fetchFnBody,
/_state:\s*\{/,
'fetchSanctionsPressure must return _state as a top-level key for extraKeys separation',
);
// Verify extraKeys is wired to write _state to its own Redis key
assert.match(
seedSrc,
/extraKeys.*STATE_KEY/s,
'extraKeys must reference STATE_KEY to write _state separately from canonical payload',
);
});
});
// ---------------------------------------------------------------------------
// P2-2: buildLocationMap must sort code/name as aligned pairs
// ---------------------------------------------------------------------------
describe('buildLocationMap: code/name alignment', () => {
it('handler buildLocationMap uses paired sort instead of independent uniqueSorted calls', () => {
const fnStart = handlerSrc.indexOf('function buildLocationMap(');
const fnEnd = handlerSrc.indexOf('\nfunction extractPartyName(');
const fnBody = handlerSrc.slice(fnStart, fnEnd);
assert.match(
fnBody,
/new Map\(mapped\.map/,
'buildLocationMap must deduplicate via Map keyed on code to preserve alignment',
);
assert.match(
fnBody,
/pairs\.map\(\(\[code\]\)/,
'buildLocationMap must derive codes from sorted pairs array',
);
assert.match(
fnBody,
/pairs\.map\(\(\[, name\]\)/,
'buildLocationMap must derive names from sorted pairs array',
);
// Must NOT independently sort codes and names
assert.ok(
!fnBody.includes('uniqueSorted(mapped.map((item) => item.code))'),
'buildLocationMap must not call uniqueSorted on codes independently',
);
assert.ok(
!fnBody.includes('uniqueSorted(mapped.map((item) => item.name))'),
'buildLocationMap must not call uniqueSorted on names independently',
);
});
it('seed buildLocationMap uses paired sort instead of independent uniqueSorted calls', () => {
const fnStart = seedSrc.indexOf('function buildLocationMap(');
const fnEnd = seedSrc.indexOf('\nfunction extractPartyName(');
const fnBody = seedSrc.slice(fnStart, fnEnd);
assert.match(
fnBody,
/new Map\(mapped\.map/,
'seed buildLocationMap must deduplicate via Map keyed on code',
);
assert.ok(
!fnBody.includes("uniqueSorted(mapped.map((item) => item.code))"),
'seed buildLocationMap must not sort codes independently',
);
assert.ok(
!fnBody.includes("uniqueSorted(mapped.map((item) => item.name))"),
'seed buildLocationMap must not sort names independently',
);
});
it('handler extractPartyCountries deduplicates via Map instead of independent uniqueSorted', () => {
const fnStart = handlerSrc.indexOf('function extractPartyCountries(');
const fnEnd = handlerSrc.indexOf('\nfunction buildPartyMap(');
const fnBody = handlerSrc.slice(fnStart, fnEnd);
assert.match(
fnBody,
/const seen = new Map/,
'extractPartyCountries must use a seen Map for deduplication',
);
assert.ok(
!fnBody.includes('uniqueSorted(codes)'),
'extractPartyCountries must not sort codes independently via uniqueSorted',
);
assert.ok(
!fnBody.includes('uniqueSorted(names)'),
'extractPartyCountries must not sort names independently via uniqueSorted',
);
});
it('seed extractPartyCountries deduplicates via Map instead of independent uniqueSorted', () => {
const fnStart = seedSrc.indexOf('function extractPartyCountries(');
const fnEnd = seedSrc.indexOf('\nfunction buildPartyMap(');
const fnBody = seedSrc.slice(fnStart, fnEnd);
assert.match(
fnBody,
/const seen = new Map/,
'seed extractPartyCountries must use a seen Map for deduplication',
);
assert.ok(
!fnBody.includes('uniqueSorted(codes)'),
'seed extractPartyCountries must not sort codes independently',
);
});
});
// ---------------------------------------------------------------------------
// P3: DEFAULT_RECENT_LIMIT must not exceed MAX_ITEMS_LIMIT
// ---------------------------------------------------------------------------
describe('sanctions seed: DEFAULT_RECENT_LIMIT vs MAX_ITEMS_LIMIT', () => {
it('seed DEFAULT_RECENT_LIMIT does not exceed handler MAX_ITEMS_LIMIT (60)', () => {
const match = seedSrc.match(/const DEFAULT_RECENT_LIMIT\s*=\s*(\d+)/);
assert.ok(match, 'DEFAULT_RECENT_LIMIT must be defined in seed script');
const seedLimit = Number(match[1]);
const handlerMatch = handlerSrc.match(/const MAX_ITEMS_LIMIT\s*=\s*(\d+)/);
assert.ok(handlerMatch, 'MAX_ITEMS_LIMIT must be defined in handler');
const handlerLimit = Number(handlerMatch[1]);
assert.ok(
seedLimit <= handlerLimit,
`DEFAULT_RECENT_LIMIT (${seedLimit}) must not exceed MAX_ITEMS_LIMIT (${handlerLimit}): entries above the handler limit are never served`,
);
});
});