@tsdmarc/opensearch 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["isRecord","MS_PER_SECOND"],"sources":["../../search-common/src/python.ts","../../search-common/src/ip.ts","../../search-common/src/dsl.ts","../../search-common/src/documents.ts","../../search-common/src/resume.ts","../src/reindex.ts","../src/transport.ts","../src/opensearch.ts"],"sourcesContent":["// Small Python semantics the port needs: `int()` / truthiness as the DSL's\n// field cleaning applies them, and dict lookups that raise `KeyError`.\n// `repr()`/`str()` come from the core (`pyRepr`, `pyStr`).\n//\n// Shared by @tsdmarc/elasticsearch and @tsdmarc/opensearch, which both bundle\n// it (parsedmarc duplicates elastic.py and opensearch.py instead).\n\nimport { camelToSnake, pyRepr } from 'tsdmarc';\n\nconst ASCII_INT = /^[+-]?\\d+(?:_\\d+)*$/u;\n\n/** Python's `KeyError`: a missing key in a report dict. */\nexport class KeyError extends Error {\n override name = 'KeyError';\n readonly key: string;\n\n constructor(key: string) {\n // str(KeyError('x')) is the repr of the key: 'x'\n super(pyRepr(key));\n this.key = key;\n }\n}\n\n/**\n * `target[key] = value` as a Python dict does it: an own enumerable\n * property even for keys from report data like `__proto__`, which plain\n * assignment would turn into a prototype change (and drop).\n */\nexport function setOwn(\n target: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n Object.defineProperty(target, key, {\n configurable: true,\n enumerable: true,\n value,\n writable: true,\n });\n}\n\n/** Python's `ValueError`. */\nexport class ValueError extends Error {\n override name = 'ValueError';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * `obj[key]` with Python's KeyError for a missing key. `key` is the camelCase\n * key of the report object; the error names the snake_case key, as parsedmarc\n * reports it.\n */\nexport function getKey(obj: unknown, key: string): unknown {\n if (!isRecord(obj) || !(key in obj)) {\n throw new KeyError(camelToSnake(key));\n }\n return obj[key];\n}\n\n/** Python's truthiness (`bool(value)`). */\nexport function pyBool(value: unknown): boolean {\n if (value === null || value === undefined || value === false) {\n return false;\n }\n if (typeof value === 'number') {\n return value !== 0 && !Number.isNaN(value);\n }\n if (typeof value === 'bigint') {\n return value !== 0n;\n }\n if (typeof value === 'string' || Array.isArray(value)) {\n return value.length > 0;\n }\n if (isRecord(value)) {\n return Object.keys(value).length > 0;\n }\n return true;\n}\n\n/** A safe integer as a number, anything larger as an exact bigint. */\nfunction exactInt(value: bigint): number | bigint {\n return value >= BigInt(Number.MIN_SAFE_INTEGER) &&\n value <= BigInt(Number.MAX_SAFE_INTEGER)\n ? Number(value)\n : value;\n}\n\n/**\n * Python's `int(value)` for the values reports carry (ASCII digits). Exact,\n * like Python's unbounded ints: beyond 2^53 the result is a `bigint`.\n */\nexport function pyInt(value: unknown): number | bigint {\n if (typeof value === 'boolean') {\n return value ? 1 : 0;\n }\n if (typeof value === 'bigint') {\n return exactInt(value);\n }\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new ValueError(\n `cannot convert float ${Number.isNaN(value) ? 'NaN' : 'infinity'} to integer`,\n );\n }\n return exactInt(BigInt(Math.trunc(value)));\n }\n if (typeof value === 'string') {\n const text = value.trim();\n if (ASCII_INT.test(text)) {\n return exactInt(BigInt(text.replaceAll('_', '')));\n }\n throw new ValueError(\n `invalid literal for int() with base 10: ${pyRepr(value)}`,\n );\n }\n throw new ValueError(\n `int() argument must be a string, a bytes-like object or a real number, not '${typeof value}'`,\n );\n}\n\nconst EXACT_INTEGER_KEY = '__tsdmarc_exact_integer__';\n// Strings are matched first so that digits inside them are left alone; an\n// integer is only taken between JSON delimiters, never inside a float.\nconst JSON_STRING_OR_LONG_INTEGER =\n /\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|(?<=[:,[\\s])-?\\d{16,}(?=[,\\]}\\s])/gu;\n\n/**\n * JSON text with integers that a double may not hold (16 digits or more)\n * wrapped, so that {@link restoreLongIntegers} can undo it after parsing.\n * Returns `text` itself when there are none.\n */\nexport function markLongIntegers(text: string): string {\n return text.replaceAll(JSON_STRING_OR_LONG_INTEGER, match =>\n match.startsWith('\"') ? match : `{\"${EXACT_INTEGER_KEY}\":\"${match}\"}`,\n );\n}\n\n/** A parsed value of {@link markLongIntegers} with exact integers back. */\nexport function restoreLongIntegers(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(item => restoreLongIntegers(item));\n }\n if (!isRecord(value)) {\n return value;\n }\n const exact = value[EXACT_INTEGER_KEY];\n if (typeof exact === 'string' && Object.keys(value).length === 1) {\n return pyInt(exact);\n }\n for (const [key, item] of Object.entries(value)) {\n setOwn(value, key, restoreLongIntegers(item));\n }\n return value;\n}\n\n/** `str(error)`: the message of an error, or the value itself. */\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n","// Python's `str(ipaddress.ip_address(value))`.\n//\n// The DSL's `Ip()` field validates every IP address before a document is\n// saved, and writes it back in Python's canonical form (lowercase, zero runs\n// compressed, IPv4-mapped IPv6 kept as `::ffff:a.b.c.d`). An invalid address\n// raises `ValueError`, which parsedmarc reports as a save error.\n//\n// Shared by @tsdmarc/elasticsearch and @tsdmarc/opensearch, which both bundle\n// it.\n\nimport { pyRepr } from 'tsdmarc';\n\nimport { ValueError } from './python';\n\nconst HEXTET_COUNT = 8;\nconst MAX_IPV6_LENGTH = 45;\nconst HEX_DIGITS = /^[0-9a-f]*$/iu;\nconst DECIMAL_DIGITS = /^\\d+$/u;\nconst IPV4_MAPPED_PREFIX = 0xff_ff;\n\n/** Parse a dotted-quad IPv4 address (strict, like Python), or `null`. */\nfunction parseIpv4(text: string): number[] | null {\n if (text === '' || text.includes('/')) {\n return null;\n }\n const octets = text.split('.');\n if (octets.length !== 4) {\n return null;\n }\n const values: number[] = [];\n for (const octet of octets) {\n if (!DECIMAL_DIGITS.test(octet) || octet.length > 3) {\n return null;\n }\n // Leading zeros are rejected, as glibc's inet_pton() does.\n if (octet !== '0' && octet.startsWith('0')) {\n return null;\n }\n const value = Number(octet);\n if (value > 255) {\n return null;\n }\n values.push(value);\n }\n return values;\n}\n\nfunction parseHextet(text: string): number | null {\n if (text === '' || !HEX_DIGITS.test(text) || text.length > 4) {\n return null;\n }\n return Number.parseInt(text, 16);\n}\n\n/** Python's `_BaseV6._ip_int_from_string`, as 8 hextets, or `null`. */\nfunction parseIpv6Hextets(text: string): number[] | null {\n if (text === '' || text.length > MAX_IPV6_LENGTH) {\n return null;\n }\n const maxParts = HEXTET_COUNT + 1;\n const parts = text.split(':');\n if (parts.length > maxParts + 1) {\n return null;\n }\n if (parts.length < 3) {\n return null;\n }\n const last = parts.at(-1) ?? '';\n if (last.includes('.')) {\n const ipv4 = parseIpv4(last);\n if (ipv4 === null) {\n return null;\n }\n parts.pop();\n const [a = 0, b = 0, c = 0, d = 0] = ipv4;\n parts.push(((a << 8) | b).toString(16), ((c << 8) | d).toString(16));\n }\n if (parts.length > maxParts) {\n return null;\n }\n let skipIndex: number | null = null;\n for (let index = 1; index < parts.length - 1; index++) {\n if (parts[index] === '') {\n if (skipIndex !== null) {\n return null;\n }\n skipIndex = index;\n }\n }\n let partsHi: number;\n let partsLo: number;\n let partsSkipped: number;\n if (skipIndex === null) {\n if (parts.length !== HEXTET_COUNT || parts[0] === '' || last === '') {\n return null;\n }\n partsHi = parts.length;\n partsLo = 0;\n partsSkipped = 0;\n } else {\n partsHi = skipIndex;\n partsLo = parts.length - skipIndex - 1;\n if (parts[0] === '') {\n partsHi -= 1;\n if (partsHi !== 0) {\n return null;\n }\n }\n if (parts.at(-1) === '') {\n partsLo -= 1;\n if (partsLo !== 0) {\n return null;\n }\n }\n partsSkipped = HEXTET_COUNT - (partsHi + partsLo);\n if (partsSkipped < 1) {\n return null;\n }\n }\n const hextets: number[] = [];\n for (let index = 0; index < partsHi; index++) {\n const value = parseHextet(parts[index] ?? '');\n if (value === null) {\n return null;\n }\n hextets.push(value);\n }\n for (let index = 0; index < partsSkipped; index++) {\n hextets.push(0);\n }\n for (let index = parts.length - partsLo; index < parts.length; index++) {\n const value = parseHextet(parts[index] ?? '');\n if (value === null) {\n return null;\n }\n hextets.push(value);\n }\n return hextets;\n}\n\n/** Python's `_compress_hextets`: replace the longest run of zeros with `::`. */\nfunction compressHextets(hextets: readonly number[]): string {\n let bestStart = -1;\n let bestLength = 0;\n let start = -1;\n let length = 0;\n hextets.forEach((hextet, index) => {\n if (hextet === 0) {\n length += 1;\n if (start === -1) {\n start = index;\n }\n if (length > bestLength) {\n bestLength = length;\n bestStart = start;\n }\n } else {\n length = 0;\n start = -1;\n }\n });\n const parts = hextets.map(hextet => hextet.toString(16));\n if (bestLength > 1) {\n const end = bestStart + bestLength;\n const head = parts.slice(0, bestStart);\n const tail = parts.slice(end);\n return `${head.join(':')}::${tail.join(':')}`;\n }\n return parts.join(':');\n}\n\nfunction formatIpv6(text: string): string | null {\n if (text.includes('/')) {\n return null;\n }\n const percent = text.indexOf('%');\n let address = text;\n let scopeId: string | null = null;\n if (percent !== -1) {\n scopeId = text.slice(percent + 1);\n address = text.slice(0, percent);\n if (scopeId === '' || scopeId.includes('%')) {\n return null;\n }\n }\n const hextets = parseIpv6Hextets(address);\n if (hextets === null) {\n return null;\n }\n const isIpv4Mapped =\n hextets.slice(0, 5).every(hextet => hextet === 0) &&\n hextets[5] === IPV4_MAPPED_PREFIX;\n const [high = 0, low = 0] = hextets.slice(6);\n const formatted = isIpv4Mapped\n ? `::ffff:${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`\n : compressHextets(hextets);\n return scopeId ? `${formatted}%${scopeId}` : formatted;\n}\n\n/**\n * Validate an IP address and return Python's canonical string for it.\n *\n * @throws {ValueError} `'<value>' does not appear to be an IPv4 or IPv6 address`\n */\nexport function normalizeIpAddress(value: string): string {\n const ipv4 = parseIpv4(value);\n if (ipv4 !== null) {\n return ipv4.join('.');\n }\n const ipv6 = formatIpv6(value);\n if (ipv6 !== null) {\n return ipv6;\n }\n throw new ValueError(\n `${pyRepr(value)} does not appear to be an IPv4 or IPv6 address`,\n );\n}\n","// The parts of elasticsearch-dsl / opensearch-dsl that parsedmarc relies on.\n//\n// parsedmarc declares its documents as DSL `Document`/`InnerDoc` classes.\n// It never installs their mappings (create_indexes() deliberately creates\n// bare indexes), but the classes still decide what a saved document looks\n// like: `Document.save()` cleans every declared field (`Integer` → int,\n// `Ip` → validated canonical address, `Date` → datetime) and serializes with\n// `to_dict(skip_empty=True)`, which drops `None`, `[]` and `{}` values at\n// each object level. This module reproduces that from a field schema, and\n// renders the schema as the mapping the DSL classes declare.\n//\n// Shared by @tsdmarc/elasticsearch and @tsdmarc/opensearch, which both bundle\n// it.\n\nimport {\n logger,\n parseDate,\n PyDateTime,\n pyRepr,\n pyStr,\n sanitizeForLog,\n} from 'tsdmarc';\n\nimport { normalizeIpAddress } from './ip';\nimport { errorMessage, pyBool, pyInt, setOwn, ValueError } from './python';\n\nexport type ScalarFieldType =\n | 'boolean'\n | 'date'\n | 'integer'\n | 'ip'\n | 'keyword'\n | 'text';\n\nexport interface SubField {\n readonly type: 'keyword';\n readonly ignore_above?: number;\n}\n\nexport interface ScalarField {\n readonly type: ScalarFieldType;\n /** Multi-fields, such as `.keyword` on the `*_combined` text fields. */\n readonly fields?: Readonly<Record<string, SubField>>;\n}\n\nexport interface ObjectField {\n readonly type: 'nested' | 'object';\n /** Declared inner fields; `undefined` for a dynamic `Object()`. */\n readonly properties?: DocumentSchema;\n /**\n * The mapping {@link indexMapping} installs for this field, beyond what\n * the DSL class declares (see `FAILURE_SAMPLE_DOC.headers`).\n */\n readonly indexMapping?: Readonly<Record<string, unknown>>;\n}\n\nexport type Field = ObjectField | ScalarField;\n\n/** Declared fields of a document, in declaration order. */\nexport type DocumentSchema = Readonly<Record<string, Field>>;\n\nexport interface IndexMapping {\n readonly date_detection: boolean;\n readonly properties: Record<string, unknown>;\n}\n\nconst MS_PER_SECOND = 1000;\n\nfunction isObjectField(field: Field): field is ObjectField {\n return field.type === 'object' || field.type === 'nested';\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object') {\n return false;\n }\n const prototype: unknown = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** `value in ([], {}, None)`: what `to_dict(skip_empty=True)` leaves out. */\nexport function isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined) {\n return true;\n }\n if (Array.isArray(value)) {\n return value.length === 0;\n }\n return isPlainObject(value) && Object.keys(value).length === 0;\n}\n\n/** The DSL's `Date._deserialize`, then the JSON serializer's `isoformat()`. */\nfunction serializeDate(value: unknown): string {\n if (value instanceof PyDateTime) {\n return value.isoformat();\n }\n if (typeof value === 'string') {\n return parseDate(value).isoformat();\n }\n if (typeof value === 'number' && Number.isInteger(value)) {\n // datetime.utcfromtimestamp(value / 1000.0): naive UTC wall clock.\n return PyDateTime.fromTimestamp(value / MS_PER_SECOND, true)\n .withOffset(null)\n .isoformat();\n }\n throw new ValueError(\n `Could not parse date from the value (${pyRepr(value)})`,\n );\n}\n\n/**\n * A report value as a text field holds it. The parser leaves an element with\n * children as an object; left alone it would map the field as an object in\n * the index (rejecting every later text value) and turn into parameters of\n * the duplicate search.\n */\nexport function textValue(value: unknown): unknown {\n return isPlainObject(value) ? pyStr(value) : value;\n}\n\n/**\n * `value`, or `null` with a warning when `clean` rejects it. For values a\n * sender controls and the parser accepts: the DSL's ValueError would fail the\n * save of the whole report on every retry, for one field nothing depends on.\n */\nfunction validOrNull(\n value: unknown,\n field: string,\n clean: (value: unknown) => unknown,\n): unknown {\n if (value === null || value === undefined) {\n return value;\n }\n try {\n clean(value);\n return value;\n } catch (error) {\n if (!(error instanceof ValueError)) {\n throw error;\n }\n logger.warning(\n `Dropping ${field} from a report: ${sanitizeForLog(errorMessage(error))}`,\n );\n return null;\n }\n}\n\n/** `value` if an `Ip()` field can hold it, otherwise `null` (see above). */\nexport function ipOrNull(value: unknown, field: string): unknown {\n return validOrNull(value, field, item => normalizeIpAddress(String(item)));\n}\n\n/** `value` if an `Integer()` field can hold it, otherwise `null` (see above). */\nexport function integerOrNull(value: unknown, field: string): unknown {\n return validOrNull(value, field, pyInt);\n}\n\nfunction serializeScalar(field: ScalarField, value: unknown): unknown {\n switch (field.type) {\n case 'integer':\n return pyInt(value);\n case 'boolean':\n return value === 'false' ? false : pyBool(value);\n case 'date':\n return serializeDate(value);\n case 'ip':\n return normalizeIpAddress(String(value));\n default:\n return textValue(value);\n }\n}\n\nfunction serializeOne(field: Field, value: unknown): unknown {\n if (isObjectField(field)) {\n // A raw dict is wrapped in an InnerDoc of the declared class (or a\n // dynamic one for a bare Object()) and serialized the same way.\n return isPlainObject(value)\n ? serializeDocument(field.properties ?? {}, value)\n : value;\n }\n return serializeScalar(field, value);\n}\n\nfunction serializeField(field: Field, value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(item =>\n item === null || item === undefined ? null : serializeOne(field, item),\n );\n }\n return serializeOne(field, value);\n}\n\n/**\n * Clean and serialize a document like `Document.save()` does: declared\n * fields are converted to their type, undeclared fields are kept as they\n * are, and empty values are dropped. Throws like the DSL does for values a\n * field cannot hold (an invalid IP address, a non-numeric integer), except\n * objects in text fields (see {@link textValue}).\n */\nexport function serializeDocument(\n schema: DocumentSchema,\n data: Readonly<Record<string, unknown>>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n const field = Object.hasOwn(schema, key) ? schema[key] : undefined;\n const serialized =\n field === undefined || value === null || value === undefined\n ? value\n : serializeField(field, value);\n if (!isEmptyValue(serialized)) {\n setOwn(out, key, serialized);\n }\n }\n return out;\n}\n\nfunction fieldMapping(field: Field): Record<string, unknown> {\n if (isObjectField(field)) {\n if (field.properties === undefined) {\n return { type: field.type };\n }\n return { ...schemaMapping(field.properties), type: field.type };\n }\n const mapping: Record<string, unknown> = { type: field.type };\n if (field.fields !== undefined) {\n mapping.fields = structuredClone(field.fields);\n }\n return mapping;\n}\n\n/** The mapping a DSL class declares (`Document._doc_type.mapping.to_dict()`). */\nexport function schemaMapping(schema: DocumentSchema): {\n properties: Record<string, unknown>;\n} {\n const properties: Record<string, unknown> = {};\n for (const [name, field] of Object.entries(schema)) {\n properties[name] = fieldMapping(field);\n }\n return { properties };\n}\n\nfunction indexProperties(schema: DocumentSchema): Record<string, unknown> {\n const properties: Record<string, unknown> = {};\n for (const [name, field] of Object.entries(schema)) {\n if (field.type === 'date') {\n properties[name] = { type: 'date' };\n } else if (isObjectField(field) && field.indexMapping !== undefined) {\n properties[name] = structuredClone(field.indexMapping);\n } else if (field.type === 'object' && field.properties !== undefined) {\n const inner = indexProperties(field.properties);\n if (Object.keys(inner).length > 0) {\n properties[name] = { properties: inner };\n }\n }\n }\n return properties;\n}\n\n/**\n * The field parsedmarc 11.2 adds to every saved document: the date that\n * picks the dated index (or that a data stream sorts by). Not declared on\n * the DSL classes, so it is serialized as given (an ISO 8601 string).\n */\nexport const TIMESTAMP_FIELD = '@timestamp';\n\n/**\n * What the index template of a data stream maps without other mappings\n * (parsedmarc's `create_data_streams()`): only {@link TIMESTAMP_FIELD}.\n */\nexport const TIMESTAMP_MAPPING: {\n readonly properties: Readonly<Record<string, unknown>>;\n} = {\n properties: { [TIMESTAMP_FIELD]: { type: 'date' } },\n};\n\n/**\n * The mappings of an index the save functions create: the declared date\n * fields, and date detection off. Otherwise the first document of an index\n * decides whether a text field is a date (`<report_id>2026-11-01</report_id>`),\n * and the index name comes from a date the sender picks too, so one report can\n * break the indexes of days to come. Plus the `indexMapping` of object fields\n * that carry one. Nothing else is declared: the dashboards need the dynamic\n * `object` mapping of the rest (see `createIndexes()`). With date detection\n * off, `@timestamp` is declared too (see {@link TIMESTAMP_FIELD}).\n */\nexport function indexMapping(schema: DocumentSchema): IndexMapping {\n return {\n date_detection: false,\n properties: { ...TIMESTAMP_MAPPING.properties, ...indexProperties(schema) },\n };\n}\n\n/**\n * The mappings of a data stream's index template: `mappings` (such as the\n * ones the save functions create dated indexes with) with\n * {@link TIMESTAMP_FIELD} as a date, which a data stream requires, or only\n * that field.\n */\nexport function dataStreamMapping(\n mappings: IndexMapping | null,\n): Record<string, unknown> {\n if (mappings === null) {\n return structuredClone(TIMESTAMP_MAPPING);\n }\n return {\n ...structuredClone(mappings),\n properties: {\n ...structuredClone(mappings.properties),\n ...structuredClone(TIMESTAMP_MAPPING.properties),\n },\n };\n}\n\n/** A query clause, as the DSL's `Q()` renders it. */\nexport type Query = Record<string, unknown>;\n\n/**\n * `q1 & q2 & ...`: the DSL combines queries into one `bool.must` list; a\n * single query stays as it is.\n */\nexport function andQueries(queries: readonly Query[]): Query {\n const [first] = queries;\n if (queries.length === 1 && first !== undefined) {\n return first;\n }\n return { bool: { must: [...queries] } };\n}\n\n/** Append to a list field, creating it on first use (an `AttrList`). */\nexport function appendTo(\n doc: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n const current = doc[key];\n if (Array.isArray(current)) {\n current.push(value);\n } else {\n doc[key] = [value];\n }\n}\n","// The document classes of parsedmarc/elastic.py and parsedmarc/opensearch.py\n// (identical in both), as field schemas.\n//\n// Field names and types match the DSL declarations one for one, including\n// their quirks: `message_count` and `original_timespan_seconds` are declared\n// as the bare `Integer` class (not an instance), so the DSL does not treat\n// them as fields; and several values parsedmarc saves (`org_name` on SMTP\n// TLS documents, `policy_string`, `source_base_domain`, ...) are not declared\n// at all. Both kinds are stored as given, so they are absent here.\n\nimport {\n type DocumentSchema,\n type Field,\n type IndexMapping,\n indexMapping,\n schemaMapping,\n} from './dsl';\n\nconst TEXT: Field = { type: 'text' };\nconst KEYWORD: Field = { type: 'keyword' };\nconst INTEGER: Field = { type: 'integer' };\nconst BOOLEAN: Field = { type: 'boolean' };\nconst DATE: Field = { type: 'date' };\nconst IP: Field = { type: 'ip' };\n\n/** `Text(multi=True, fields={\"keyword\": Keyword(ignore_above=256)})`. */\nconst COMBINED_TEXT: Field = {\n type: 'text',\n fields: { keyword: { type: 'keyword', ignore_above: 256 } },\n};\n\nconst POLICY_OVERRIDE_DOC: DocumentSchema = { type: TEXT, comment: TEXT };\n\nconst PUBLISHED_POLICY_DOC: DocumentSchema = {\n domain: TEXT,\n adkim: TEXT,\n aspf: TEXT,\n p: TEXT,\n sp: TEXT,\n pct: INTEGER,\n fo: TEXT,\n np: KEYWORD,\n testing: KEYWORD,\n discovery_method: KEYWORD,\n};\n\nconst DKIM_RESULT_DOC: DocumentSchema = {\n domain: TEXT,\n selector: TEXT,\n result: TEXT,\n human_result: TEXT,\n};\n\nconst SPF_RESULT_DOC: DocumentSchema = {\n domain: TEXT,\n scope: TEXT,\n result: TEXT,\n human_result: TEXT,\n};\n\n/** `_AggregateReportDoc` (index `dmarc_aggregate`). */\nexport const AGGREGATE_REPORT_DOC: DocumentSchema = {\n xml_schema: TEXT,\n xml_namespace: KEYWORD,\n org_name: TEXT,\n org_email: TEXT,\n org_extra_contact_info: TEXT,\n report_id: TEXT,\n date_range: DATE,\n date_begin: DATE,\n date_end: DATE,\n normalized_timespan: BOOLEAN,\n errors: TEXT,\n published_policy: { type: 'object', properties: PUBLISHED_POLICY_DOC },\n source_ip_address: IP,\n source_country: TEXT,\n source_reverse_dns: TEXT,\n source_base_domain: TEXT,\n source_type: TEXT,\n source_name: TEXT,\n source_asn: INTEGER,\n source_as_name: TEXT,\n source_as_domain: TEXT,\n disposition: TEXT,\n dkim_aligned: BOOLEAN,\n spf_aligned: BOOLEAN,\n passed_dmarc: BOOLEAN,\n policy_overrides: { type: 'nested', properties: POLICY_OVERRIDE_DOC },\n header_from: TEXT,\n envelope_from: TEXT,\n envelope_to: TEXT,\n // Nested only in the DSL's in-memory shape: create_indexes() never\n // installs this mapping, so the cluster maps these as plain `object`\n // (parsedmarc issue #169).\n dkim_results: { type: 'nested', properties: DKIM_RESULT_DOC },\n spf_results: { type: 'nested', properties: SPF_RESULT_DOC },\n dkim_results_combined: COMBINED_TEXT,\n spf_results_combined: COMBINED_TEXT,\n np: KEYWORD,\n testing: KEYWORD,\n discovery_method: KEYWORD,\n generator: TEXT,\n};\n\nconst EMAIL_ADDRESS_DOC: DocumentSchema = { display_name: TEXT, address: TEXT };\n\n/** `_EmailAttachmentDoc`. */\nexport const EMAIL_ATTACHMENT_DOC: DocumentSchema = {\n filename: TEXT,\n content_type: TEXT,\n sha256: TEXT,\n};\n\n/**\n * The `sample.headers` fields a failure index maps: those the shipped\n * Grafana and OpenSearch dashboards (`dashboards/`) know, and a few more\n * that help to trace a message. Header names come from the sender, and with\n * dynamic mapping each new one adds a field until the index reaches\n * `index.mapping.total_fields.limit` (1,000 by default), after which every\n * sample with an unmapped header is rejected until the next index.\n */\nexport const INDEXED_SAMPLE_HEADERS: readonly string[] = [\n 'authentication-results',\n 'auto-submitted',\n 'cc',\n 'content-transfer-encoding',\n 'content-type',\n 'date',\n 'delivered-to',\n 'dkim-signature',\n 'from',\n 'in-reply-to',\n 'list-id',\n 'message-id',\n 'mime-version',\n 'received',\n 'received-spf',\n 'references',\n 'reply-to',\n 'return-path',\n 'sender',\n 'subject',\n 'thread-index',\n 'thread-topic',\n 'to',\n 'user-agent',\n 'x-auto-response-suppress',\n 'x-exclaimer-md-config',\n 'x-linkedin-fe',\n 'x-mailer',\n 'x-ms-exchange-generated-message-source',\n 'x-ms-exchange-inbox-rules-loop',\n 'x-ms-exchange-parent-message-id',\n 'x-ms-exchange-transport-fromentityheader',\n 'x-ms-has-attach',\n 'x-ms-tnef-correlator',\n 'x-onpremexternalip',\n 'x-originating-ip',\n];\n\n/**\n * What dynamic mapping gives a string, so that the listed headers map as in\n * parsedmarc (`sample.headers.from.keyword`). `dynamic: false` keeps every\n * other header in `_source`, without a field: visible, not searchable.\n */\nconst SAMPLE_HEADERS_MAPPING = {\n dynamic: false,\n properties: Object.fromEntries(\n INDEXED_SAMPLE_HEADERS.map(name => [\n name,\n {\n type: 'text',\n fields: { keyword: { type: 'keyword', ignore_above: 256 } },\n },\n ]),\n ),\n};\n\n/** `_FailureSampleDoc`. */\nexport const FAILURE_SAMPLE_DOC: DocumentSchema = {\n raw: TEXT,\n // A dynamic Object() in the DSL; the save functions map it as above.\n headers: { type: 'object', indexMapping: SAMPLE_HEADERS_MAPPING },\n headers_only: BOOLEAN,\n to: { type: 'nested', properties: EMAIL_ADDRESS_DOC },\n subject: TEXT,\n filename_safe_subject: TEXT,\n _from: { type: 'object', properties: EMAIL_ADDRESS_DOC },\n date: DATE,\n reply_to: { type: 'nested', properties: EMAIL_ADDRESS_DOC },\n cc: { type: 'nested', properties: EMAIL_ADDRESS_DOC },\n bcc: { type: 'nested', properties: EMAIL_ADDRESS_DOC },\n body: TEXT,\n attachments: { type: 'nested', properties: EMAIL_ATTACHMENT_DOC },\n};\n\n/** `_FailureReportDoc` (index `dmarc_failure`). */\nexport const FAILURE_REPORT_DOC: DocumentSchema = {\n feedback_type: TEXT,\n user_agent: TEXT,\n version: TEXT,\n original_mail_from: TEXT,\n arrival_date: DATE,\n domain: TEXT,\n original_envelope_id: TEXT,\n authentication_results: TEXT,\n delivery_results: TEXT,\n source_ip_address: IP,\n source_country: TEXT,\n source_reverse_dns: TEXT,\n source_asn: INTEGER,\n source_as_name: TEXT,\n source_as_domain: TEXT,\n source_authentication_mechanisms: TEXT,\n source_auth_failures: TEXT,\n dkim_domain: TEXT,\n original_rcpt_to: TEXT,\n sample: { type: 'object', properties: FAILURE_SAMPLE_DOC },\n};\n\nconst SMTP_TLS_FAILURE_DETAILS_DOC: DocumentSchema = {\n result_type: TEXT,\n sending_mta_ip: IP,\n receiving_mx_helo: TEXT,\n receiving_mx_hostname: TEXT,\n receiving_ip: IP,\n failed_session_count: INTEGER,\n additional_information_uri: TEXT,\n failure_reason_code: TEXT,\n};\n\nconst SMTP_TLS_POLICY_DOC: DocumentSchema = {\n policy_domain: TEXT,\n policy_type: TEXT,\n policy_strings: TEXT,\n mx_host_patterns: TEXT,\n successful_session_count: INTEGER,\n failed_session_count: INTEGER,\n failure_details: { type: 'nested', properties: SMTP_TLS_FAILURE_DETAILS_DOC },\n};\n\n/** `_SMTPTLSReportDoc` (index `smtp_tls`). */\nexport const SMTP_TLS_REPORT_DOC: DocumentSchema = {\n organization_name: TEXT,\n date_range: DATE,\n date_begin: DATE,\n date_end: DATE,\n contact_info: TEXT,\n report_id: TEXT,\n policies: { type: 'nested', properties: SMTP_TLS_POLICY_DOC },\n policies_combined: COMBINED_TEXT,\n failure_details_combined: COMBINED_TEXT,\n};\n\nexport interface DocumentMappings {\n readonly aggregate: { properties: Record<string, unknown> };\n readonly failure: { properties: Record<string, unknown> };\n readonly smtpTls: { properties: Record<string, unknown> };\n}\n\n/**\n * The mappings parsedmarc's document classes declare. `createIndexes()`\n * does not install them (neither does parsedmarc): the shipped dashboards\n * rely on dynamic mapping. They are exported for reference and for users\n * who manage index templates themselves.\n */\nexport const DOCUMENT_MAPPINGS: DocumentMappings = {\n aggregate: schemaMapping(AGGREGATE_REPORT_DOC),\n failure: schemaMapping(FAILURE_REPORT_DOC),\n smtpTls: schemaMapping(SMTP_TLS_REPORT_DOC),\n};\n\n/** What the save functions create indexes with (see `indexMapping()`). */\nexport const INDEX_MAPPINGS: {\n readonly aggregate: IndexMapping;\n readonly failure: IndexMapping;\n readonly smtpTls: IndexMapping;\n} = {\n aggregate: indexMapping(AGGREGATE_REPORT_DOC),\n failure: indexMapping(FAILURE_REPORT_DOC),\n smtpTls: indexMapping(SMTP_TLS_REPORT_DOC),\n};\n","// Resuming an aggregate report whose save stopped halfway. Elasticsearch and\n// OpenSearch store one document per record, written one after the other, and\n// parsedmarc takes any document found by the duplicate search as the whole\n// report: after a write failed on record k, the retry raised AlreadySaved and\n// the records from k on were never stored. Here the documents found are\n// compared with the report's own, and a retry writes only the missing ones.\n\nimport { PyDateTime } from 'tsdmarc';\n\n/**\n * The fields that tell the documents of one report apart. Enrichment\n * (reverse DNS, GeoIP, source name and type) is left out: its answers\n * change between attempts.\n */\nexport const AGGREGATE_RECORD_FIELDS: readonly string[] = [\n 'date_begin',\n 'date_end',\n 'source_ip_address',\n 'message_count',\n 'disposition',\n 'dkim_aligned',\n 'spf_aligned',\n 'header_from',\n 'envelope_from',\n 'envelope_to',\n 'dkim_results_combined',\n 'spf_results_combined',\n];\n\n/**\n * Most records a report can have to be resumed: the stored documents are\n * read in one search page, which the default `index.max_result_window`\n * limits to 10,000 hits (one more than the report is asked for).\n */\nconst MAX_RESUMABLE_RECORDS = 9999;\n\nfunction normalized(value: unknown): unknown {\n if (value === null || value === undefined) {\n return null;\n }\n if (value instanceof PyDateTime) {\n return value.isoformat();\n }\n if (Array.isArray(value)) {\n return value.map(normalized);\n }\n if (typeof value === 'number' || typeof value === 'bigint') {\n return String(value);\n }\n if (typeof value === 'object') {\n // Nothing a resumed document holds in these fields: never equal.\n return { object: String(value) };\n }\n return value;\n}\n\n/** A serialized record document as the duplicate search returns it. */\nfunction fingerprint(document: unknown): string {\n const source =\n typeof document === 'object' && document !== null\n ? (document as Record<string, unknown>)\n : {};\n return JSON.stringify(\n AGGREGATE_RECORD_FIELDS.map(field =>\n normalized(Object.hasOwn(source, field) ? source[field] : null),\n ),\n );\n}\n\n/** Hits in a search response without `size`: Elasticsearch's default. */\nconst DEFAULT_SEARCH_SIZE = 10;\n\n/**\n * The positions of the report's records that are already stored, when the\n * documents the duplicate search found are part of this report and some are\n * missing. `null` means the report counts as saved (parsedmarc's\n * `AlreadySaved`): every record is there, the documents found are not all\n * this report's (another report with the same identity), or there are too\n * many to compare.\n *\n * @param documents The report's documents, serialized as they are sent, in\n * record order; `null` when they can't be serialized.\n * @param found The `_source` of the hits of the duplicate search (one page\n * of the default size).\n * @param fetchStored Reads the `_source` (at least\n * {@link AGGREGATE_RECORD_FIELDS}) of up to `size` documents the duplicate\n * search matches, when `found` is a full page.\n */\nexport async function findStoredRecords(\n documents: readonly Record<string, unknown>[] | null,\n found: readonly unknown[],\n fetchStored: (size: number) => Promise<readonly unknown[]>,\n): Promise<ReadonlySet<number> | null> {\n if (\n documents === null ||\n found.length === 0 ||\n found.length >= documents.length ||\n documents.length > MAX_RESUMABLE_RECORDS\n ) {\n return null;\n }\n const stored =\n found.length < DEFAULT_SEARCH_SIZE\n ? found\n : await fetchStored(documents.length + 1);\n if (stored.length === 0 || stored.length >= documents.length) {\n return null;\n }\n const positions = new Map<string, number[]>();\n for (const [position, document] of documents.entries()) {\n const key = fingerprint(document);\n const list = positions.get(key);\n if (list === undefined) {\n positions.set(key, [position]);\n } else {\n list.push(position);\n }\n }\n const taken = new Set<number>();\n for (const source of stored) {\n const position = positions.get(fingerprint(source))?.shift();\n if (position === undefined) {\n return null;\n }\n taken.add(position);\n }\n return taken;\n}\n","// Port of opensearchpy.helpers.reindex() (with scan() and bulk()), which\n// parsedmarc's legacy `published_policy.fo` migration uses: scroll through\n// the source index in `_doc` order and bulk-index every hit, with the same\n// `_id`, into the target index.\n\nimport type { Client } from '@opensearch-project/opensearch';\nimport { rawJsonIntegers } from 'tsdmarc';\n\nconst DEFAULT_SCROLL = '5m';\nconst SCAN_SIZE = 1000;\nconst BULK_CHUNK_SIZE = 500;\nconst BULK_MAX_CHUNK_BYTES = 100 * 1024 * 1024;\nconst HTTP_NOT_FOUND = 404;\n// Hit metadata that expand_action() moves into the bulk action line.\nconst ACTION_META_KEYS = ['_id', '_index', '_routing'] as const;\n\n/** Raised when a scroll request fails on some shards (helpers.ScanError). */\nexport class ScanError extends Error {\n override name = 'ScanError';\n readonly scrollId: string;\n\n constructor(scrollId: string, message: string) {\n super(message);\n this.scrollId = scrollId;\n }\n}\n\n/** Raised when some documents fail to index (helpers.BulkIndexError). */\nexport class BulkIndexError extends Error {\n override name = 'BulkIndexError';\n readonly errors: unknown[];\n\n constructor(message: string, errors: unknown[]) {\n super(message);\n this.errors = errors;\n }\n}\n\ninterface Hit {\n _id?: string;\n _index?: string;\n _routing?: string;\n _source?: unknown;\n [key: string]: unknown;\n}\n\ninterface ScrollResponse {\n _scroll_id?: string;\n _shards?: { total?: number; successful?: number; skipped?: number };\n hits?: { hits?: Hit[] };\n}\n\ninterface BulkResponse {\n items?: Record<string, { status?: number } | undefined>[];\n}\n\n/**\n * Unlike helpers.scan(), also checked on a page without hits: when the last\n * shard with documents fails, the scroll ends in an empty page that reports\n * the failure, and stopping there would lose those documents in silence.\n */\nfunction assertAllShards(response: ScrollResponse, scrollId: string): void {\n const shards = response._shards;\n if (shards) {\n const successful = shards.successful ?? 0;\n const skipped = shards.skipped ?? 0;\n const total = shards.total ?? 0;\n if (successful + skipped < total) {\n throw new ScanError(\n scrollId,\n `Scroll request has only succeeded on ${successful} (+${skipped} skipped) shards out of ${total}.`,\n );\n }\n }\n}\n\nasync function* scan(\n client: Client,\n index: string,\n scroll: string,\n): AsyncGenerator<Hit> {\n let response = (\n await client.search({\n index,\n scroll,\n size: SCAN_SIZE,\n body: { sort: '_doc' },\n })\n ).body as ScrollResponse;\n let scrollId = response._scroll_id;\n try {\n while (scrollId) {\n const hits = response.hits?.hits ?? [];\n yield* hits;\n assertAllShards(response, scrollId);\n if (hits.length === 0) {\n break;\n }\n response = (\n await client.scroll({ body: { scroll_id: scrollId, scroll } })\n ).body as ScrollResponse;\n scrollId = response._scroll_id;\n }\n } finally {\n if (scrollId) {\n await client.clearScroll(\n { body: { scroll_id: [scrollId] } },\n { ignore: [HTTP_NOT_FOUND] },\n );\n }\n }\n}\n\n/** expand_action(): one hit → the bulk action line and the document. */\nfunction expandHit(hit: Hit): [Record<string, unknown>, unknown] {\n const meta: Record<string, unknown> = {};\n for (const key of ACTION_META_KEYS) {\n if (key in hit) {\n meta[key] = hit[key];\n }\n }\n // The client's long numeral support keeps integers beyond 2^53 as bigints.\n return [\n { index: meta },\n rawJsonIntegers('_source' in hit ? hit._source : hit),\n ];\n}\n\nasync function sendChunk(client: Client, body: unknown[]): Promise<void> {\n const response = (await client.bulk({ body: body as never[] }))\n .body as BulkResponse;\n if (!response.items) {\n throw new BulkIndexError('The bulk response has no items.', []);\n }\n const errors: unknown[] = [];\n for (const item of response.items) {\n for (const [opType, result] of Object.entries(item)) {\n const status = result?.status ?? 500;\n if (status < 200 || status >= 300) {\n errors.push({ [opType]: result });\n }\n }\n }\n if (errors.length > 0) {\n throw new BulkIndexError(\n `${errors.length} document(s) failed to index.`,\n errors,\n );\n }\n}\n\n/**\n * Copy every document of `sourceIndex` into `targetIndex`\n * (`opensearchpy.helpers.reindex(client, source_index, target_index)`).\n */\nexport async function reindex(\n client: Client,\n sourceIndex: string,\n targetIndex: string,\n): Promise<void> {\n let body: unknown[] = [];\n let count = 0;\n let bytes = 0;\n for await (const hit of scan(client, sourceIndex, DEFAULT_SCROLL)) {\n const [action, document] = expandHit({ ...hit, _index: targetIndex });\n const size =\n Buffer.byteLength(JSON.stringify(action)) +\n Buffer.byteLength(JSON.stringify(document)) +\n 2;\n if (\n count > 0 &&\n (count === BULK_CHUNK_SIZE || bytes + size > BULK_MAX_CHUNK_BYTES)\n ) {\n await sendChunk(client, body);\n body = [];\n count = 0;\n bytes = 0;\n }\n body.push(action, document);\n count += 1;\n bytes += size;\n }\n if (count > 0) {\n await sendChunk(client, body);\n }\n}\n","// opensearch-py's retry policy for opensearch-js.\n//\n// opensearch-py's Transport retries a request (max_retries=3) on a\n// connection error and on HTTP 502/503/504, but not on a timeout\n// (retry_on_timeout=False). opensearch-js retries timeouts too, with no\n// option to turn that off: a write that timed out but then succeeded on the\n// server would be sent again and stored twice. This transport runs each\n// attempt with the client's own retries off and retries like Python.\n\nimport {\n type ApiError,\n type ApiResponse,\n errors,\n type Transport,\n} from '@opensearch-project/opensearch';\n\ntype TransportClass = typeof Transport;\ntype TransportRequestParams = Parameters<Transport['request']>[0];\ntype TransportRequestOptions = NonNullable<Parameters<Transport['request']>[1]>;\ntype RequestCallback = (error: ApiError, result: ApiResponse) => void;\ninterface AbortablePromise extends Promise<ApiResponse> {\n abort: () => void;\n}\n\n/** opensearch-py's `Transport.DEFAULT_RETRY_ON_STATUS`. */\nconst RETRY_ON_STATUS: ReadonlySet<number> = new Set([502, 503, 504]);\n\nfunction isStream(value: unknown): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { pipe?: unknown }).pipe === 'function'\n );\n}\n\n/** Whether opensearch-py would retry after `error`. */\nexport function isRetryable(error: unknown): boolean {\n if (error instanceof errors.TimeoutError) {\n return false;\n }\n if (error instanceof errors.ConnectionError) {\n return true;\n }\n return (\n error instanceof errors.ResponseError &&\n RETRY_ON_STATUS.has(error.statusCode)\n );\n}\n\n/** Hand the outcome of `promise` to a node-style callback. */\nasync function deliver(\n promise: Promise<ApiResponse>,\n callback: RequestCallback,\n): Promise<void> {\n let result: ApiResponse;\n try {\n result = await promise;\n } catch (error) {\n callback(error as ApiError, (error as { meta: ApiResponse }).meta);\n return;\n }\n // opensearch-js passes null on success; its types leave that out.\n callback(null as unknown as ApiError, result);\n}\n\n/**\n * A subclass of `Base` (the client's Transport, or the SigV4 signer's) that\n * never retries a timed-out request.\n */\nexport function withPythonRetries(Base: TransportClass): TransportClass {\n class PythonRetryTransport extends Base {\n override request(\n params: TransportRequestParams,\n options: TransportRequestOptions | RequestCallback = {},\n callback?: RequestCallback,\n ): AbortablePromise {\n let requestOptions: TransportRequestOptions = {};\n let done = callback;\n if (typeof options === 'function') {\n done = options;\n } else {\n requestOptions = options;\n }\n // Like opensearch-js, never resend a stream body.\n const maxRetries =\n isStream(params.body) || isStream(params.bulkBody)\n ? 0\n : (requestOptions.maxRetries ?? this.maxRetries);\n let current: { abort: () => void } | null = null;\n let aborted = false;\n const attempt = (): Promise<ApiResponse> => {\n const pending = super.request(params, {\n ...requestOptions,\n maxRetries: 0,\n });\n current = pending;\n return pending;\n };\n const run = async (): Promise<ApiResponse> => {\n for (let retries = 0; ; retries += 1) {\n try {\n // oxlint-disable-next-line no-await-in-loop -- attempts are sequential\n return await attempt();\n } catch (error) {\n if (aborted || retries >= maxRetries || !isRetryable(error)) {\n throw error;\n }\n }\n }\n };\n const promise = run();\n const abort = (): void => {\n aborted = true;\n current?.abort();\n };\n if (done) {\n deliver(promise, done).catch((error: unknown) => {\n // A throwing callback surfaces as an uncaught exception, as it\n // does with opensearch-js's own callbacks.\n queueMicrotask(() => {\n throw error;\n });\n });\n }\n return Object.assign(promise, { abort });\n }\n }\n return PythonRetryTransport as TransportClass;\n}\n","// Port of parsedmarc/opensearch.py.\n//\n// parsedmarc builds documents with opensearch-py's DSL classes and saves\n// them through the DSL's global `default` connection. Here the documents are\n// plain objects cleaned and serialized like the DSL does (see ./dsl and\n// ./documents), and the client registered by setHosts() plays the part of\n// the `default` connection.\n\nimport { readFile } from 'node:fs/promises';\n\nimport {\n Client,\n type ClientOptions,\n errors,\n Transport,\n} from '@opensearch-project/opensearch';\nimport {\n AGGREGATE_REPORT_DOC,\n FAILURE_REPORT_DOC,\n INDEX_MAPPINGS,\n SMTP_TLS_REPORT_DOC,\n} from '@tsdmarc/search-common/documents';\nimport {\n andQueries,\n appendTo,\n dataStreamMapping,\n integerOrNull,\n ipOrNull,\n serializeDocument,\n textValue,\n TIMESTAMP_FIELD,\n type DocumentSchema,\n type IndexMapping,\n type Query,\n} from '@tsdmarc/search-common/dsl';\nimport {\n errorMessage,\n getKey,\n KeyError,\n setOwn,\n ValueError,\n} from '@tsdmarc/search-common/python';\nimport {\n AGGREGATE_RECORD_FIELDS,\n findStoredRecords,\n} from '@tsdmarc/search-common/resume';\nimport {\n humanTimestampToDatetime,\n InvalidFailureReport,\n logger,\n pyRepr,\n pyStr,\n rawJsonIntegers,\n redactUrlCredentials,\n sanitizeForLog,\n type AggregateRecord,\n type AggregateReport,\n type FailureReport,\n type PyDateTime,\n type SMTPTLSReport,\n} from 'tsdmarc';\n\nimport { reindex } from './reindex';\nimport { withPythonRetries } from './transport';\n\n/** Raised when an OpenSearch error occurs. */\nexport class OpenSearchError extends Error {\n override name = 'OpenSearchError';\n}\n\n/** Raised when a report to be saved matches an existing report. */\nexport class AlreadySaved extends Error {\n override name = 'AlreadySaved';\n}\n\nconst MS_PER_SECOND = 1000;\nconst DEFAULT_MAX_INDEXES_PER_REPORT = 31;\nconst DEFAULT_TIMEOUT_SECONDS = 60;\nconst DEFAULT_PORT = 9200;\nconst HTTPS_PORT = 443;\n\n// The client's mapping types only describe concrete field types; the\n// object-with-properties shape below is valid but not expressible there.\ntype PutMappingBody = NonNullable<\n Parameters<Client['indices']['putMapping']>[0]\n>['body'];\n\ntype CreateIndexMappings = NonNullable<\n NonNullable<Parameters<Client['indices']['create']>[0]>['body']\n>['mappings'];\n\n// The single `default` connection (opensearch-dsl `connections`).\nlet defaultConnection: Client | null = null;\n\n// Priority of the index template createDataStreams() installs. OpenSearch\n// itself installs no index templates, but plugins may; 501 stays above the\n// usual priority-100 convention and matches the Elasticsearch package. The\n// template's `index_patterns` is the exact stream name, so an overlap with\n// another template is unlikely in the first place.\nconst DATA_STREAM_TEMPLATE_PRIORITY = 501;\n\ntype PutIndexTemplateBody = NonNullable<\n Parameters<Client['indices']['putIndexTemplate']>[0]\n>['body'];\n\ninterface DataStreamState {\n /**\n * The template each stream was set up with, by stream name, so each one\n * costs its API calls once per client rather than once per saved\n * document, and again when the template changes (other settings or\n * mappings). parsedmarc keeps one set of names per process and clears it\n * in set_hosts(); a client from setHosts() or createClient() starts\n * empty here. Forgetting an entry is always safe: it only repeats\n * idempotent calls.\n */\n readonly ready: Map<string, string>;\n /** Set-ups in progress, so concurrent saves to a new stream share one. */\n readonly pending: Map<string, Promise<void>>;\n}\n\nconst dataStreamStates = new WeakMap<Client, DataStreamState>();\n\n// Guard query for the dkim_results_combined/spf_results_combined backfill\n// (see migrateIndexes()). Matches only documents that have at least one DKIM\n// or SPF auth result and are missing the corresponding combined field. Empty\n// arrays are invisible to `exists`, so documents with zero DKIM/SPF results\n// are skipped, and the query is idempotent: a backfilled document no longer\n// matches. Each result is matched on an OR of its `domain`/`result`\n// subfields as defense in depth (an empty string indexes no tokens and is\n// invisible to `exists`).\nconst COMBINED_BACKFILL_QUERY: Query = {\n bool: {\n minimum_should_match: 1,\n should: [\n {\n bool: {\n must: [\n {\n bool: {\n minimum_should_match: 1,\n should: [\n { exists: { field: 'dkim_results.domain' } },\n { exists: { field: 'dkim_results.result' } },\n ],\n },\n },\n ],\n must_not: [{ exists: { field: 'dkim_results_combined' } }],\n },\n },\n {\n bool: {\n must: [\n {\n bool: {\n minimum_should_match: 1,\n should: [\n { exists: { field: 'spf_results.domain' } },\n { exists: { field: 'spf_results.result' } },\n ],\n },\n },\n ],\n must_not: [{ exists: { field: 'spf_results_combined' } }],\n },\n },\n ],\n },\n};\n\n// Painless script that (re)derives dkim_results_combined/spf_results_combined\n// from dkim_results/spf_results, in the format saveAggregateReportToOpenSearch()\n// writes: \"{selector} / {domain} / {result}\" per DKIM result and\n// \"{scope} / {domain} / {result}\" per SPF result.\nconst COMBINED_BACKFILL_SCRIPT = [\n 'List dk = new ArrayList(); ',\n 'def dr = ctx._source.dkim_results; ',\n 'if (dr != null) { ',\n 'if (!(dr instanceof List)) { dr = [dr]; } ',\n 'for (e in dr) { ',\n 'if (e == null) { continue; } ',\n 'def sel = e.selector != null ? e.selector : \"none\"; ',\n 'def dom = e.domain != null ? e.domain : \"none\"; ',\n 'def res = e.result != null ? e.result : \"none\"; ',\n 'dk.add(sel + \" / \" + dom + \" / \" + res); ',\n '} } ',\n 'ctx._source.dkim_results_combined = dk; ',\n 'List sp = new ArrayList(); ',\n 'def sr = ctx._source.spf_results; ',\n 'if (sr != null) { ',\n 'if (!(sr instanceof List)) { sr = [sr]; } ',\n 'for (e in sr) { ',\n 'if (e == null) { continue; } ',\n 'def sc = e.scope != null ? e.scope : \"mfrom\"; ',\n 'def dom = e.domain != null ? e.domain : \"none\"; ',\n 'def res = e.result != null ? e.result : (e.results != null ? e.results : \"none\"); ',\n 'sp.add(sc + \" / \" + dom + \" / \" + res); ',\n '} } ',\n 'ctx._source.spf_results_combined = sp;',\n].join('');\n\n// Guard query for the policies_combined/failure_details_combined backfill\n// (see migrateIndexes()), on the same principles as COMBINED_BACKFILL_QUERY.\nconst SMTP_TLS_COMBINED_BACKFILL_QUERY: Query = {\n bool: {\n minimum_should_match: 1,\n should: [\n {\n bool: {\n must: [\n {\n bool: {\n minimum_should_match: 1,\n should: [\n { exists: { field: 'policies.policy_domain' } },\n { exists: { field: 'policies.policy_type' } },\n ],\n },\n },\n ],\n must_not: [{ exists: { field: 'policies_combined' } }],\n },\n },\n {\n bool: {\n must: [\n {\n bool: {\n minimum_should_match: 1,\n should: [\n {\n exists: { field: 'policies.failure_details.result_type' },\n },\n {\n exists: {\n field: 'policies.failure_details.sending_mta_ip',\n },\n },\n ],\n },\n },\n ],\n must_not: [{ exists: { field: 'failure_details_combined' } }],\n },\n },\n ],\n },\n};\n\n// Painless script that (re)derives policies_combined/failure_details_combined\n// from policies/policies.failure_details, in the format\n// saveSmtpTlsReportToOpenSearch() writes.\nconst SMTP_TLS_COMBINED_BACKFILL_SCRIPT = [\n 'List pols = new ArrayList(); ',\n 'List dets = new ArrayList(); ',\n 'def ps = ctx._source.policies; ',\n 'if (ps != null) { ',\n 'if (!(ps instanceof List)) { ps = [ps]; } ',\n 'for (p in ps) { ',\n 'if (p == null) { continue; } ',\n 'def dom = p.policy_domain != null ? p.policy_domain : \"none\"; ',\n 'def typ = p.policy_type != null ? p.policy_type : \"none\"; ',\n 'pols.add(dom + \" / \" + typ); ',\n 'def fds = p.failure_details; ',\n 'if (fds != null) { ',\n 'if (!(fds instanceof List)) { fds = [fds]; } ',\n 'for (f in fds) { ',\n 'if (f == null) { continue; } ',\n 'def rt = f.result_type != null ? f.result_type : \"none\"; ',\n 'def smi = f.sending_mta_ip != null ? f.sending_mta_ip : \"none\"; ',\n 'def ri = f.receiving_ip != null ? f.receiving_ip : \"none\"; ',\n 'def rmh = f.receiving_mx_hostname != null ? f.receiving_mx_hostname : \"none\"; ',\n 'dets.add(dom + \" / \" + typ + \" / \" + rt + \" / \" + smi + \" / \" + ri + \" / \" + rmh); ',\n '} } } } ',\n 'ctx._source.policies_combined = pols; ',\n 'ctx._source.failure_details_combined = dets;',\n].join('');\n\n/** The backfill queries and scripts migrateIndexes() submits. */\nexport const BACKFILL: {\n readonly aggregateQuery: Query;\n readonly aggregateScript: string;\n readonly smtpTlsQuery: Query;\n readonly smtpTlsScript: string;\n} = {\n aggregateQuery: COMBINED_BACKFILL_QUERY,\n aggregateScript: COMBINED_BACKFILL_SCRIPT,\n smtpTlsQuery: SMTP_TLS_COMBINED_BACKFILL_QUERY,\n smtpTlsScript: SMTP_TLS_COMBINED_BACKFILL_SCRIPT,\n};\n\nexport interface SetHostsOptions {\n /** Use an HTTPS connection to the server. */\n useSsl?: boolean | null;\n /** Path to the CA certificate chain (used when `useSsl` is set). */\n sslCertPath?: string | null;\n /** Skip certificate verification (used when `useSsl` is set). */\n skipCertificateVerification?: boolean;\n username?: string | null;\n password?: string | null;\n /**\n * The Base64-encoded API key. Accepted as parsedmarc accepts it, but\n * opensearch-py ignores a client-level `api_key`, so parsedmarc never\n * sends it; neither does tsdmarc.\n */\n apiKey?: string | null;\n /**\n * Request timeout in seconds (default 60). `null` means no timeout; 0 or\n * less raises `ValueError`, like opensearch-py.\n */\n timeout?: number | null;\n /** `basic` (default) or `awssigv4`. */\n authType?: string;\n /** AWS region for SigV4 auth (required for `awssigv4`). */\n awsRegion?: string | null;\n /** AWS service for SigV4 signing (default `es`; `aoss` for Serverless). */\n awsService?: string;\n}\n\nconst NETLOC_END = /[/?#]/u;\nconst TRAILING_PORT = /:(\\d+)$/u;\n\n/**\n * opensearch-py's host normalization: a host without a scheme is read as\n * `//host`; `https://` hosts default to port 443 and TLS, others to port\n * 9200; the client-level `use_ssl` turns on TLS for every host.\n */\nfunction nodeUrl(host: string, useSsl: boolean): string {\n const withScheme = host.includes('://') ? host : `//${host}`;\n const parsed = new URL(withScheme, 'http://placeholder');\n const schemeIsHttps = withScheme.toLowerCase().startsWith('https://');\n const netloc = withScheme.split('//', 2)[1]?.split(NETLOC_END, 1)[0] ?? '';\n const hostPort = netloc.slice(netloc.lastIndexOf('@') + 1);\n const portPart = hostPort.startsWith('[')\n ? hostPort.slice(hostPort.indexOf(']') + 1)\n : hostPort;\n const explicitPort = TRAILING_PORT.exec(portPart)?.[1];\n const port =\n explicitPort ?? String(schemeIsHttps ? HTTPS_PORT : DEFAULT_PORT);\n const scheme = schemeIsHttps || useSsl ? 'https' : 'http';\n const userinfo =\n parsed.username || parsed.password\n ? `${parsed.username}:${parsed.password}@`\n : '';\n const path = parsed.pathname === '/' ? '' : parsed.pathname;\n return `${scheme}://${userinfo}${parsed.hostname}:${port}${path}`;\n}\n\n/**\n * urllib3's timeout rules, as opensearch-py applies them: `None` means no\n * timeout (0 for opensearch-js), and a value that is not positive raises.\n */\nfunction requestTimeoutMs(timeout: number | null): number {\n if (timeout === null) {\n return 0;\n }\n if (!(timeout > 0)) {\n throw new ValueError(\n `Attempted to set connect timeout to ${timeout}, but the timeout cannot be set to a value less than or equal to 0.`,\n );\n }\n return timeout * MS_PER_SECOND;\n}\n\nasync function sigV4Options(\n awsRegion: string | null,\n awsService: string,\n): Promise<Partial<ClientOptions>> {\n if (!awsRegion) {\n throw new OpenSearchError(\n \"OpenSearch AWS SigV4 auth requires 'aws_region' to be set\",\n );\n }\n // Loaded only for SigV4, so basic-auth users never load the AWS SDK.\n const [{ fromNodeProviderChain }, { AwsSigv4Signer }] = await Promise.all([\n import('@aws-sdk/credential-providers'),\n import('@opensearch-project/opensearch/aws'),\n ]);\n const getCredentials = fromNodeProviderChain();\n try {\n // Fail now, like boto3's get_credentials() returning None, rather than\n // with an opaque authentication error on the first request.\n await getCredentials();\n } catch {\n throw new OpenSearchError(\n 'Unable to load AWS credentials for OpenSearch SigV4 authentication',\n );\n }\n // The signer's types only list 'es' and 'aoss'; parsedmarc passes the\n // configured service through as is.\n return AwsSigv4Signer({\n region: awsRegion,\n service: awsService as 'es',\n getCredentials,\n });\n}\n\n/**\n * Creates an OpenSearch client the way {@link setHosts} does, without\n * registering it as the default connection. Pass it as `client` to the save\n * functions, createIndexes() and migrateIndexes() to write to several\n * clusters from one process. Close it with `client.close()`.\n */\nexport async function createClient(\n hosts: string | string[],\n options: SetHostsOptions = {},\n): Promise<Client> {\n const {\n useSsl = false,\n sslCertPath = null,\n skipCertificateVerification = false,\n username = null,\n password = null,\n timeout = DEFAULT_TIMEOUT_SECONDS,\n authType = 'basic',\n awsRegion = null,\n awsService = 'es',\n } = options;\n const hostList = Array.isArray(hosts) ? hosts : [hosts];\n logger.debug(\n `Connecting to OpenSearch: hosts=${pyRepr(redactUrlCredentials(hostList))}, use_ssl=${pyRepr(useSsl)}`,\n );\n const clientOptions: ClientOptions = {\n nodes: hostList.map(host => nodeUrl(host, Boolean(useSsl))),\n // As Python's JSON parser: integers beyond 2^53 stay exact (the legacy\n // migration writes back the documents it reads), and `__proto__` is just\n // a key: a stored failure sample can hold it as a header name, and the\n // duplicate search reads that document back. Responses are never merged\n // into other objects.\n enableLongNumeralSupport: true,\n disablePrototypePoisoningProtection: true,\n };\n if (useSsl) {\n clientOptions.ssl = {\n rejectUnauthorized: !skipCertificateVerification,\n ...(sslCertPath ? { ca: await readFile(sslCertPath) } : {}),\n };\n }\n const normalizedAuthType = (authType || 'basic').trim().toLowerCase();\n if (normalizedAuthType === 'awssigv4') {\n Object.assign(clientOptions, await sigV4Options(awsRegion, awsService));\n } else if (normalizedAuthType === 'basic') {\n if (username && password) {\n clientOptions.auth = { username, password };\n }\n } else {\n throw new OpenSearchError(\n `Unsupported OpenSearch auth_type '${authType}'. ` +\n \"Expected 'basic' or 'awssigv4'.\",\n );\n }\n // After the auth checks, where opensearch-py's constructor would raise.\n clientOptions.requestTimeout = requestTimeoutMs(timeout);\n clientOptions.Transport = withPythonRetries(\n clientOptions.Transport ?? Transport,\n );\n return new Client(clientOptions);\n}\n\n/**\n * Sets the OpenSearch hosts to use, and registers the client as the\n * default connection the other functions use.\n *\n * A client registered by an earlier call is replaced but not closed, as in\n * parsedmarc (the DSL's connection registry only drops it): its owner may\n * still be using it or put it back (the CLI's configuration reload). Close\n * clients you no longer need with `client.close()`; on shutdown,\n * `await getDefaultConnection()?.close()` then `setDefaultConnection(null)`.\n *\n * @returns The registered client. Callers that need to close this exact\n * client later should hold on to it.\n */\nexport async function setHosts(\n hosts: string | string[],\n options: SetHostsOptions = {},\n): Promise<Client> {\n defaultConnection = await createClient(hosts, options);\n return defaultConnection;\n}\n\n/**\n * The client registered as the `default` connection, or `null`. With\n * {@link setDefaultConnection}, lets a caller snapshot and restore the\n * module state setHosts() writes (the CLI's configuration reload).\n */\nexport function getDefaultConnection(): Client | null {\n return defaultConnection;\n}\n\n/** Register `client` as the `default` connection (`null` removes it). */\nexport function setDefaultConnection(client: Client | null): void {\n defaultConnection = client;\n}\n\n/** Selects the cluster a function writes to. */\nexport interface ConnectionOptions {\n /**\n * The client to use instead of the default connection setHosts()\n * registers, such as one from {@link createClient}. The default connection\n * is module state, shared by every caller in the process: an application\n * that writes to several clusters at once passes the client of each.\n */\n client?: Client | null;\n}\n\n/**\n * `client`, or the client registered by setHosts() (DSL\n * `connections.get_connection()`).\n */\nfunction getConnection(client?: Client | null): Client {\n if (client) {\n return client;\n }\n if (defaultConnection === null) {\n throw new KeyError(\"There is no connection with alias 'default'.\");\n }\n return defaultConnection;\n}\n\nasync function indexExists(client: Client, name: string): Promise<boolean> {\n return (await client.indices.exists({ index: name })).body;\n}\n\n/** `Index(name).settings(...).create()`: the body is `{}` without settings. */\nasync function createIndex(\n client: Client,\n name: string,\n settings: Record<string, unknown> = {},\n mappings: IndexMapping | null = null,\n): Promise<void> {\n await client.indices.create({\n index: name,\n body: {\n ...(Object.keys(settings).length > 0 ? { settings } : {}),\n ...(mappings ? { mappings: mappings as CreateIndexMappings } : {}),\n },\n });\n}\n\n/**\n * Whether `error` is the cluster refusing to create an index that another\n * writer created after the exists check (concurrent saves to a new index).\n */\nfunction isIndexAlreadyCreated(error: unknown): boolean {\n if (!(error instanceof errors.ResponseError)) {\n return false;\n }\n const body = error.meta.body as { error?: { type?: unknown } } | undefined;\n return body?.error?.type === 'resource_already_exists_exception';\n}\n\n/**\n * Create OpenSearch indexes that don't exist yet.\n *\n * @param settings Index settings (default: 1 shard, 0 replicas).\n * @param mappings Mappings for new indexes (default: none, as in parsedmarc).\n * @param options The cluster to use (default: the default connection).\n */\nexport async function createIndexes(\n names: string[],\n settings: Record<string, unknown> | null = null,\n mappings: IndexMapping | null = null,\n options: ConnectionOptions = {},\n): Promise<void> {\n for (const name of names) {\n try {\n // Deliberately no `nested` mappings: the shipped dashboards cannot\n // rebuild their detail tables on them, so the dynamic `object` mapping\n // is load-bearing (parsedmarc issue #169; see the *_combined fields).\n const client = getConnection(options.client);\n if (!(await indexExists(client, name))) {\n logger.debug(`Creating OpenSearch index: ${name}`);\n try {\n await createIndex(\n client,\n name,\n settings ?? { number_of_shards: 1, number_of_replicas: 0 },\n mappings,\n );\n } catch (error) {\n // parsedmarc saves one report at a time; concurrent saves race\n // between the exists check and the create, and the loser would\n // fail its report halfway through its records.\n if (!isIndexAlreadyCreated(error)) {\n throw error;\n }\n }\n }\n } catch (error) {\n throw new OpenSearchError(`OpenSearch error: ${errorMessage(error)}`);\n }\n }\n}\n\nfunction dataStreamState(client: Client): DataStreamState {\n let state = dataStreamStates.get(client);\n if (state === undefined) {\n state = { ready: new Map(), pending: new Map() };\n dataStreamStates.set(client, state);\n }\n return state;\n}\n\n/**\n * Whether `error` is the cluster refusing to look up a data stream by the\n * name of an alias (`illegal_argument_exception`, HTTP 400). The index\n * exists check then reports the name as taken, with a clearer message than\n * this error.\n */\nfunction isAliasName(error: unknown): boolean {\n if (!(error instanceof errors.ResponseError) || error.statusCode !== 400) {\n return false;\n }\n const body = error.meta.body as { error?: { type?: unknown } } | undefined;\n return body?.error?.type === 'illegal_argument_exception';\n}\n\n/** Whether `error` is an HTTP 404 (opensearch-py's `NotFoundError`). */\nfunction isNotFound(error: unknown): boolean {\n return error instanceof errors.ResponseError && error.statusCode === 404;\n}\n\n/**\n * Whether a data stream holds `name`. OpenSearch 3 answers both a missing\n * name and a name held by a concrete index with a 404. Elasticsearch answers\n * the latter with 200 and an empty list, so, as in the Elasticsearch\n * package, look for the stream in the response rather than trust the status\n * alone.\n */\nasync function dataStreamExists(\n client: Client,\n name: string,\n): Promise<boolean> {\n try {\n const response = await client.indices.getDataStream({ name });\n const streams = (response.body as { data_streams?: { name?: unknown }[] })\n .data_streams;\n return (streams ?? []).some(stream => stream.name === name);\n } catch (error) {\n if (isNotFound(error) || isAliasName(error)) {\n return false;\n }\n throw error;\n }\n}\n\nasync function setUpDataStream(\n client: Client,\n name: string,\n body: PutIndexTemplateBody,\n): Promise<void> {\n let collision = false;\n try {\n let streamExists = await dataStreamExists(client, name);\n // Checked before anything is written, so a name collision leaves no\n // template behind.\n if (!streamExists && (await indexExists(client, name))) {\n // Another writer may have created the stream between the two checks.\n streamExists = await dataStreamExists(client, name);\n collision = !streamExists;\n }\n if (!collision) {\n await client.indices.putIndexTemplate({ name, body });\n if (!streamExists) {\n logger.debug(`Creating OpenSearch data stream: ${name}`);\n try {\n await client.indices.createDataStream({ name });\n } catch (error) {\n // Concurrent saves race between the check and the create.\n if (!isIndexAlreadyCreated(error)) {\n throw error;\n }\n }\n }\n }\n } catch (error) {\n throw new OpenSearchError(`OpenSearch error: ${errorMessage(error)}`);\n }\n if (collision) {\n throw new OpenSearchError(\n `OpenSearch error: cannot create the data stream ${name}: ` +\n 'an index or alias with that name already exists',\n );\n }\n}\n\n/**\n * Create OpenSearch data streams and their index templates.\n *\n * For each name, puts an index template named after the stream whose\n * `index_patterns` is exactly that name (no wildcard, so it never applies to\n * the date-suffixed indexes written without data streams), and then creates\n * the data stream if it does not exist yet. The template is put again by\n * every process that writes to the stream, so edits made to it directly do\n * not last; an ISM policy attaches itself to the stream's backing indices\n * through its own `ism_template` instead. Each name is set up once per\n * client and template.\n *\n * @param settings Index settings for the stream's backing indices (default:\n * 1 shard, 0 replicas, as in {@link createIndexes}).\n * @param mappings Mappings for the backing indices, plus `@timestamp` as a\n * date (default: only `@timestamp`, as in parsedmarc). The save functions\n * pass the mappings they create dated indexes with.\n * @param options The cluster to use (default: the default connection).\n * @throws {OpenSearchError} when a concrete index or alias already holds one\n * of the names, or the cluster returns an error. A name that fails is not\n * remembered, so the next save retries it.\n */\nexport async function createDataStreams(\n names: string[],\n settings: Record<string, unknown> | null = null,\n mappings: IndexMapping | null = null,\n options: ConnectionOptions = {},\n): Promise<void> {\n const effectiveSettings = settings\n ? { ...settings }\n : { number_of_shards: 1, number_of_replicas: 0 };\n\n for (const name of names) {\n let client: Client;\n try {\n client = getConnection(options.client);\n } catch (error) {\n throw new OpenSearchError(`OpenSearch error: ${errorMessage(error)}`);\n }\n const body = {\n index_patterns: [name],\n data_stream: {},\n priority: DATA_STREAM_TEMPLATE_PRIORITY,\n template: {\n settings: effectiveSettings,\n mappings: dataStreamMapping(mappings),\n },\n _meta: {\n description:\n 'Managed by tsdmarc, which overwrites it. ' +\n 'Attach ISM policies with an ism_template.',\n },\n } as PutIndexTemplateBody;\n const key = JSON.stringify({ name, body });\n const state = dataStreamState(client);\n if (state.ready.get(name) === key) {\n continue;\n }\n let pending = state.pending.get(key);\n if (pending === undefined) {\n pending = setUpDataStream(client, name, body).finally(() => {\n state.pending.delete(key);\n });\n state.pending.set(key, pending);\n }\n await pending;\n state.ready.set(name, key);\n }\n}\n\nconst LEGACY_FO_FIELD = 'published_policy.fo';\n// The same field split into the object/leaf names a mapping body nests it\n// under, so the mapping written below cannot drift from the field read.\nconst [LEGACY_FO_OBJECT = '', LEGACY_FO_LEAF = ''] = LEGACY_FO_FIELD.split('.');\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * The mapped type of `published_policy.fo` in an index, or `null` when the\n * index does not map it. Reads both the typeless response (OpenSearch has\n * no mapping types) and the Elasticsearch 6-era shape keyed by type (`doc`).\n */\nasync function legacyFoFieldType(\n client: Client,\n index: string,\n): Promise<string | null> {\n const response = (\n await client.indices.getFieldMapping({ index, fields: [LEGACY_FO_FIELD] })\n ).body as Record<string, { mappings: Record<string, unknown> }>;\n const [firstIndex = ''] = Object.keys(response);\n let mappings: Record<string, unknown> = response[firstIndex]?.mappings ?? {};\n if (!(LEGACY_FO_FIELD in mappings)) {\n for (const typeName of ['doc', '_doc']) {\n const typed = mappings[typeName];\n if (isRecord(typed)) {\n mappings = typed;\n break;\n }\n }\n }\n const fieldMapping = mappings[LEGACY_FO_FIELD];\n if (!isRecord(fieldMapping) || Object.keys(fieldMapping).length === 0) {\n return null;\n }\n const mapping = isRecord(fieldMapping.mapping) ? fieldMapping.mapping : {};\n const leaf = isRecord(mapping[LEGACY_FO_LEAF]) ? mapping[LEGACY_FO_LEAF] : {};\n return typeof leaf.type === 'string' ? leaf.type : null;\n}\n\nexport interface MigrateIndexesOptions extends ConnectionOptions {\n /** Aggregate index names (backfilled as the `name*` pattern). */\n aggregateIndexes?: string[] | null;\n /** Failure index names (accepted for API compatibility; unused). */\n failureIndexes?: string[] | null;\n /** SMTP TLS index names (backfilled as the `name*` pattern). */\n smtpTlsIndexes?: string[] | null;\n /**\n * Exact index names to check for the pre-5.0.0 `published_policy.fo`\n * `long` mapping, as the caller's prefix/suffix configuration names them.\n */\n legacyFoIndexes?: string[] | null;\n}\n\nasync function migrateLegacyFoIndex(\n client: Client,\n legacyIndexName: string,\n version: number,\n): Promise<void> {\n if (!(await indexExists(client, legacyIndexName))) {\n return;\n }\n if ((await legacyFoFieldType(client, legacyIndexName)) !== 'long') {\n return;\n }\n const newIndexName = `${legacyIndexName}-v${version}`;\n // Nested object form rather than a dotted key: dot expansion depends on\n // the object field's `subobjects` setting, and this shape never does.\n const body = {\n properties: {\n [LEGACY_FO_OBJECT]: {\n properties: {\n [LEGACY_FO_LEAF]: {\n type: 'text',\n fields: { keyword: { type: 'keyword', ignore_above: 256 } },\n },\n },\n },\n },\n };\n logger.info(\n `Migrating ${legacyIndexName} to ${newIndexName}: ` +\n `${LEGACY_FO_FIELD} is mapped as long, as parsedmarc ` +\n 'releases before 5.0.0 declared it',\n );\n // The original is deleted only after the reindex succeeds, so a leftover\n // target index is debris of an interrupted attempt; keeping it would fail\n // every later attempt with \"resource already exists\".\n if (await indexExists(client, newIndexName)) {\n logger.warning(\n `Discarding ${newIndexName} left behind by an earlier ` +\n `interrupted migration of ${legacyIndexName}`,\n );\n await client.indices.delete({ index: newIndexName });\n }\n await createIndex(client, newIndexName);\n await client.indices.putMapping({\n index: newIndexName,\n body: body as PutMappingBody,\n });\n await reindex(client, legacyIndexName, newIndexName);\n await verifyCopy(client, legacyIndexName, newIndexName);\n await client.indices.delete({ index: legacyIndexName });\n}\n\n/** Documents in `index`, failing if some shard did not answer. */\nasync function countDocuments(client: Client, index: string): Promise<number> {\n const { body: response } = await client.count({ index });\n if ((response._shards?.failed ?? 0) > 0) {\n throw new OpenSearchError(`Could not count the documents of ${index}`);\n }\n return response.count;\n}\n\n/** The original is about to be deleted: the copy must be complete. */\nasync function verifyCopy(\n client: Client,\n sourceIndex: string,\n targetIndex: string,\n): Promise<void> {\n await client.indices.refresh({ index: targetIndex });\n const sourceCount = await countDocuments(client, sourceIndex);\n const targetCount = await countDocuments(client, targetIndex);\n if (targetCount !== sourceCount) {\n throw new OpenSearchError(\n `${targetIndex} holds ${targetCount} documents, ` +\n `${sourceIndex} holds ${sourceCount}`,\n );\n }\n}\n\ninterface BackfillTarget {\n readonly names: readonly string[];\n readonly query: Query;\n readonly script: string;\n readonly fields: string;\n}\n\nasync function submitBackfills(\n client: Client,\n target: BackfillTarget,\n): Promise<void> {\n for (const name of target.names) {\n const pattern = `${name}*`;\n try {\n const countResponse = await client.count({\n index: pattern,\n body: { query: target.query },\n ignore_unavailable: true,\n allow_no_indices: true,\n });\n const { count } = countResponse.body as { count?: number };\n if (!count) {\n continue;\n }\n const updateResponse = await client.updateByQuery({\n index: pattern,\n body: {\n query: target.query,\n script: { source: target.script, lang: 'painless' },\n },\n conflicts: 'proceed',\n wait_for_completion: false,\n ignore_unavailable: true,\n allow_no_indices: true,\n });\n const taskId =\n (updateResponse.body as { task?: string } | undefined)?.task ?? null;\n logger.info(\n `Backfilling ${target.fields} on ` +\n `${count} existing documents in ${pattern} (task ${pyStr(taskId)})`,\n );\n } catch (error) {\n logger.warning(\n `Failed to check/submit the ${target.fields} backfill for ` +\n `${pattern}: ${errorMessage(error)}. This ` +\n 'will be retried at the next startup; the manual ' +\n '_update_by_query command in the documentation remains ' +\n 'available in the meantime.',\n );\n }\n }\n}\n\n/**\n * Runs index migrations and backfills.\n *\n * First, the legacy `published_policy.fo` migration for each name in\n * `legacyFoIndexes`: an index that maps the field as `long` (parsedmarc\n * before 5.0.0) is rebuilt as a `-v2` index with the text/keyword shape, its\n * documents are reindexed into it, and the original is deleted.\n *\n * Second, it backfills `dkim_results_combined`/`spf_results_combined` on\n * aggregate documents and `policies_combined`/`failure_details_combined` on\n * SMTP TLS documents saved before those fields existed, as non-blocking\n * `update_by_query` tasks against each `name*` pattern, guarded by a cheap\n * `count`. Errors are logged as warnings, never thrown: the work is retried\n * at the next startup.\n */\nexport async function migrateIndexes(\n options: MigrateIndexesOptions = {},\n): Promise<void> {\n const { aggregateIndexes, smtpTlsIndexes, legacyFoIndexes } = options;\n const hasAggregate = Boolean(aggregateIndexes?.length);\n const hasSmtpTls = Boolean(smtpTlsIndexes?.length);\n if (!hasAggregate && !hasSmtpTls && !legacyFoIndexes?.length) {\n return;\n }\n\n const version = 2;\n for (const legacyIndexName of legacyFoIndexes ?? []) {\n try {\n await migrateLegacyFoIndex(\n getConnection(options.client),\n legacyIndexName,\n version,\n );\n } catch (error) {\n logger.warning(\n 'Failed the legacy published_policy.fo migration for ' +\n `${legacyIndexName}: ${errorMessage(error)}. This will be retried at the ` +\n 'next startup.',\n );\n }\n }\n\n if (!hasAggregate && !hasSmtpTls) {\n return;\n }\n\n let client: Client;\n try {\n client = getConnection(options.client);\n } catch (error) {\n logger.warning(\n 'Skipping the dkim_results_combined/spf_results_combined/' +\n 'policies_combined/failure_details_combined backfill: could ' +\n `not get an OpenSearch connection: ${errorMessage(error)}. This will be retried ` +\n 'at the next startup.',\n );\n return;\n }\n await submitBackfills(client, {\n names: aggregateIndexes ?? [],\n query: COMBINED_BACKFILL_QUERY,\n script: COMBINED_BACKFILL_SCRIPT,\n fields: 'dkim_results_combined/spf_results_combined',\n });\n await submitBackfills(client, {\n names: smtpTlsIndexes ?? [],\n query: SMTP_TLS_COMBINED_BACKFILL_QUERY,\n script: SMTP_TLS_COMBINED_BACKFILL_SCRIPT,\n fields: 'policies_combined/failure_details_combined',\n });\n}\n\nexport interface SaveOptions extends ConnectionOptions {\n /** The suffix of the name of the index to save to. */\n indexSuffix?: string | null;\n /** The prefix of the name of the index to save to. */\n indexPrefix?: string | null;\n /**\n * Use monthly indexes instead of daily indexes (ignored when\n * `useDataStreams` is set).\n */\n monthlyIndexes?: boolean | null;\n /** The number of shards to use in the index (default 1). */\n numberOfShards?: number;\n /** The number of replicas to use in the index (default 0). */\n numberOfReplicas?: number;\n /**\n * Most indexes one aggregate report may spread over (default 31, 0 = no\n * limit). The sender picks the dates, and each new index takes a shard from\n * the cluster's limit; the records past this go to the report's first\n * index, with their dates intact.\n */\n maxIndexesPerReport?: number;\n /**\n * Save to a data stream named like the index, without the date suffix,\n * instead of to a dated index (see {@link createDataStreams}).\n */\n useDataStreams?: boolean;\n}\n\n/** `Search(index=...).query(...).execute()`: the number of hits. */\nasync function countExisting(\n options: ConnectionOptions,\n index: string,\n query: Query,\n): Promise<number> {\n return (await searchExisting(options, index, query)).length;\n}\n\n/** The `_source` of the hits of `Search(index=...).query(...).execute()`. */\nasync function searchExisting(\n options: ConnectionOptions,\n index: string,\n query: Query,\n): Promise<unknown[]> {\n const response = await getConnection(options.client).search({\n index,\n // The undated name in the list is missing until a data stream holds it.\n ignore_unavailable: true,\n body: { query },\n });\n return (response.body.hits.hits as { _source?: unknown }[]).map(\n hit => hit._source,\n );\n}\n\n/** The `_source` of up to `size` documents a duplicate search matches. */\nasync function storedSources(\n options: ConnectionOptions,\n index: string,\n query: Query,\n size: number,\n): Promise<unknown[]> {\n const response = await getConnection(options.client).search({\n index,\n ignore_unavailable: true,\n body: { query, size, _source: [...AGGREGATE_RECORD_FIELDS] },\n });\n return (response.body.hits.hits as { _source?: unknown }[]).map(\n hit => hit._source,\n );\n}\n\n/** `Document.save()`: clean, serialize and index the document. */\nasync function saveDocument(\n options: ConnectionOptions,\n schema: DocumentSchema,\n index: string,\n doc: Record<string, unknown>,\n): Promise<void> {\n // The client serializes with JSON.stringify(), which throws on bigints;\n // Python sends counts beyond 2^53 as exact JSON integers.\n const body = rawJsonIntegers(serializeDocument(schema, doc)) as Record<\n string,\n unknown\n >;\n // Data streams accept only op_type=create. No _id is set, so for a dated\n // index it means the same as the default.\n await getConnection(options.client).index({\n index,\n body,\n op_type: 'create',\n });\n}\n\n/** The index name without its date: also the name of the data stream. */\nfunction undatedIndexName(base: string, options: SaveOptions): string {\n let index = base;\n if (options.indexSuffix) {\n index = `${index}_${options.indexSuffix}`;\n }\n if (options.indexPrefix) {\n index = `${options.indexPrefix}${index}`;\n }\n return index;\n}\n\n/**\n * The indexes a duplicate search covers: the dated indexes of this prefix\n * and suffix, and the data stream (or a pre-5.0.0 undated index) of the\n * same name, so duplicates are caught across a switch between the two. The\n * `-` before the date stops the wildcard at the suffix, so `prod` does not\n * reach `production` and no suffix does not reach every suffix.\n */\nfunction searchPattern(base: string, options: SaveOptions): string {\n const name = undatedIndexName(base, options);\n return `${name}-*,${name}`;\n}\n\nfunction indexSettings(options: SaveOptions): Record<string, unknown> {\n return {\n number_of_shards: options.numberOfShards ?? 1,\n number_of_replicas: options.numberOfReplicas ?? 0,\n };\n}\n\nfunction indexDate(date: PyDateTime, monthly: boolean | null | undefined) {\n return date.strftime(monthly ? '%Y-%m' : '%Y-%m-%d');\n}\n\nfunction isAligned(result: string | null | undefined): boolean {\n return (\n result !== null && result !== undefined && result.toLowerCase() === 'pass'\n );\n}\n\n/** The indexes of one report, at most `maxIndexesPerReport` of them. */\nclass ReportIndexes {\n readonly #limit: number;\n readonly #names = new Set<string>();\n #warned = false;\n\n constructor(options: SaveOptions) {\n this.#limit = options.maxIndexesPerReport ?? DEFAULT_MAX_INDEXES_PER_REPORT;\n }\n\n /** `name`, or the report's first index once the limit is reached. */\n pick(name: string): string {\n if (\n this.#limit === 0 ||\n this.#names.has(name) ||\n this.#names.size < this.#limit\n ) {\n this.#names.add(name);\n return name;\n }\n const [first = name] = this.#names;\n if (!this.#warned) {\n this.#warned = true;\n logger.warning(\n `An aggregate report spans more than ${this.#limit} indexes; ` +\n `saving the remaining records to ${first}`,\n );\n }\n return first;\n }\n}\n\n// parsedmarc's records carry `normalized_timespan`; the core type may not\n// declare it yet.\ntype RecordWithTimespan = AggregateRecord & { normalizedTimespan?: boolean };\n\n/**\n * Saves a parsed DMARC aggregate report to OpenSearch, one document per\n * record.\n *\n * @throws {AlreadySaved} when the report is already in OpenSearch.\n * @throws {OpenSearchError} when OpenSearch fails or rejects the document.\n */\nexport async function saveAggregateReportToOpenSearch(\n aggregateReport: AggregateReport,\n options: SaveOptions = {},\n): Promise<void> {\n logger.info('Saving aggregate report to OpenSearch');\n const metadata = aggregateReport.reportMetadata;\n const policy = aggregateReport.policyPublished;\n const { orgName, reportId } = metadata;\n const domain = textValue(policy.domain);\n const beginDate = humanTimestampToDatetime(metadata.beginDate, {\n toUtc: true,\n });\n const endDate = humanTimestampToDatetime(metadata.endDate, { toUtc: true });\n\n const searchIndex = searchPattern('dmarc_aggregate', options);\n const query = andQueries([\n { match_phrase: { org_name: orgName } },\n { match_phrase: { report_id: reportId } },\n { match_phrase: { 'published_policy.domain': domain } },\n { range: { date_begin: { gte: beginDate.isoformat() } } },\n { range: { date_end: { lte: endDate.isoformat() } } },\n ]);\n const beginDateHuman = beginDate.strftime('%Y-%m-%d %H:%M:%SZ');\n const endDateHuman = endDate.strftime('%Y-%m-%d %H:%M:%SZ');\n\n let existing: unknown[];\n try {\n existing = await searchExisting(options, searchIndex, query);\n } catch (error) {\n throw new OpenSearchError(\n `OpenSearch's search for existing report error: ${errorMessage(error)}`,\n );\n }\n const alreadySaved = (): AlreadySaved =>\n new AlreadySaved(\n `An aggregate report ID ${reportId} from ${orgName} about ${pyStr(domain)} ` +\n `with a date range of ${beginDateHuman} UTC to ${endDateHuman} UTC already ` +\n 'exists in OpenSearch',\n );\n const records = aggregateReport.records as RecordWithTimespan[];\n const recordDocument = (\n record: RecordWithTimespan,\n policyDocument: Record<string, unknown> | null,\n ): { aggDoc: Record<string, unknown>; recordBegin: PyDateTime } => {\n const recordBegin = humanTimestampToDatetime(record.intervalBegin, {\n toUtc: true,\n assumeUtc: true,\n });\n const recordEnd = humanTimestampToDatetime(record.intervalEnd, {\n toUtc: true,\n assumeUtc: true,\n });\n const { source, policyEvaluated, identifiers } = record;\n const aggDoc: Record<string, unknown> = {\n xml_schema: aggregateReport.xmlSchema,\n xml_namespace: aggregateReport.xmlNamespace ?? null,\n org_name: metadata.orgName,\n org_email: metadata.orgEmail,\n org_extra_contact_info: metadata.orgExtraContactInfo,\n report_id: metadata.reportId,\n date_range: [recordBegin, recordEnd],\n date_begin: recordBegin,\n date_end: recordEnd,\n normalized_timespan: record.normalizedTimespan ?? null,\n errors: metadata.errors,\n published_policy: policyDocument,\n source_ip_address: source.ipAddress,\n source_country: source.country,\n source_reverse_dns: source.reverseDns,\n source_base_domain: source.baseDomain,\n source_type: source.type,\n source_name: source.name,\n source_asn: source.asn,\n source_as_name: source.asName,\n source_as_domain: source.asDomain,\n message_count: record.count,\n disposition: policyEvaluated.disposition,\n dkim_aligned: isAligned(policyEvaluated.dkim),\n spf_aligned: isAligned(policyEvaluated.spf),\n header_from: identifiers.headerFrom,\n envelope_from: identifiers.envelopeFrom,\n envelope_to: identifiers.envelopeTo,\n np: policy.np ?? null,\n testing: policy.testing ?? null,\n discovery_method: policy.discoveryMethod ?? null,\n generator: metadata.generator ?? null,\n [TIMESTAMP_FIELD]: recordBegin.isoformat(),\n };\n\n for (const override of policyEvaluated.policyOverrideReasons) {\n appendTo(aggDoc, 'policy_overrides', {\n type: override.type,\n comment: override.comment,\n });\n }\n for (const dkim of record.authResults.dkim) {\n appendTo(aggDoc, 'dkim_results', {\n domain: dkim.domain,\n selector: dkim.selector,\n result: dkim.result,\n human_result: dkim.humanResult ?? null,\n });\n appendTo(\n aggDoc,\n 'dkim_results_combined',\n `${dkim.selector} / ${dkim.domain} / ${dkim.result}`,\n );\n }\n for (const spf of record.authResults.spf) {\n appendTo(aggDoc, 'spf_results', {\n domain: spf.domain,\n scope: spf.scope,\n result: spf.result,\n human_result: spf.humanResult ?? null,\n });\n appendTo(\n aggDoc,\n 'spf_results_combined',\n `${spf.scope} / ${spf.domain} / ${spf.result}`,\n );\n }\n return { aggDoc, recordBegin };\n };\n\n // Deviation: parsedmarc takes any document found as the whole report, so\n // a retry after a write failed halfway lost the remaining records. When\n // what was found is part of this report, only the rest is written.\n let storedRecords: ReadonlySet<number> = new Set();\n if (existing.length > 0) {\n // Only a report with more records than documents found can be partial.\n let documents: Record<string, unknown>[] | null = null;\n if (existing.length < records.length) {\n try {\n documents = records.map(record =>\n serializeDocument(\n AGGREGATE_REPORT_DOC,\n recordDocument(record, null).aggDoc,\n ),\n );\n } catch {\n // Its write would fail as well: counted as saved, as before.\n documents = null;\n }\n }\n let stored: ReadonlySet<number> | null;\n try {\n stored = await findStoredRecords(documents, existing, async size =>\n storedSources(options, searchIndex, query, size),\n );\n } catch (error) {\n throw new OpenSearchError(\n `OpenSearch's search for existing report error: ${errorMessage(error)}`,\n );\n }\n if (stored === null) {\n throw alreadySaved();\n }\n storedRecords = stored;\n logger.warning(\n `Resuming aggregate report ID ${sanitizeForLog(pyStr(reportId))} from ` +\n `${sanitizeForLog(pyStr(orgName))}: ${stored.size} of ${records.length} ` +\n 'records are already in OpenSearch',\n );\n }\n const publishedPolicy = {\n domain: policy.domain,\n adkim: policy.adkim,\n aspf: policy.aspf,\n p: policy.p,\n sp: policy.sp,\n pct: integerOrNull(policy.pct, 'pct'),\n fo: policy.fo,\n np: policy.np ?? null,\n testing: policy.testing ?? null,\n discovery_method: policy.discoveryMethod ?? null,\n };\n\n const indexes = new ReportIndexes(options);\n for (const [position, record] of records.entries()) {\n const { aggDoc, recordBegin } = recordDocument(record, publishedPolicy);\n let index = undatedIndexName('dmarc_aggregate', options);\n if (!options.useDataStreams) {\n // Picked for stored records too, so the others keep their index.\n index = indexes.pick(\n `${index}-${indexDate(recordBegin, options.monthlyIndexes)}`,\n );\n }\n if (storedRecords.has(position)) {\n continue;\n }\n if (options.useDataStreams) {\n await createDataStreams(\n [index],\n indexSettings(options),\n INDEX_MAPPINGS.aggregate,\n options,\n );\n } else {\n await createIndexes(\n [index],\n indexSettings(options),\n INDEX_MAPPINGS.aggregate,\n options,\n );\n }\n\n try {\n // _AggregateReportDoc.save() derives passed_dmarc from the alignment.\n aggDoc.passed_dmarc = aggDoc.spf_aligned || aggDoc.dkim_aligned;\n await saveDocument(options, AGGREGATE_REPORT_DOC, index, aggDoc);\n } catch (error) {\n throw new OpenSearchError(`OpenSearch error: ${errorMessage(error)}`);\n }\n }\n}\n\n/**\n * Flatten a mailparser address header (`[[display_name, address], ...]`) to\n * one string from its first address: the bare address when the display\n * name is empty, otherwise `Name <address>`. `null` when there is no address\n * (`From:`, `To: undisclosed-recipients:;`), which parsedmarc fails on.\n */\nfunction flattenAddressHeader(value: unknown): unknown {\n if (pyLength(value) === 0) {\n return null;\n }\n const first = pyIndex(value, 0);\n if (pyIndex(first, 0) === '') {\n return pyIndex(first, 1);\n }\n return `${pyJoin(' <', first)}>`;\n}\n\nfunction pyLength(value: unknown): number | null {\n return Array.isArray(value) || typeof value === 'string'\n ? value.length\n : null;\n}\n\n/** `value[index]` for the lists and strings header values can be. */\nfunction pyIndex(value: unknown, index: number): unknown {\n if (Array.isArray(value) || typeof value === 'string') {\n if (index >= value.length) {\n throw new Error('list index out of range');\n }\n return value[index];\n }\n throw new TypeError(`'${typeof value}' object is not subscriptable`);\n}\n\n/** `separator.join(value)` for a list of strings or a string. */\nfunction pyJoin(separator: string, value: unknown): string {\n if (typeof value === 'string') {\n return [...value].join(separator);\n }\n if (Array.isArray(value)) {\n return value.map(String).join(separator);\n }\n throw new TypeError('can only join an iterable');\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return isRecord(value) ? value : {};\n}\n\n/**\n * Saves a parsed DMARC failure report to OpenSearch.\n *\n * @throws {AlreadySaved} when the failure sample is already in OpenSearch.\n * @throws {OpenSearchError} when OpenSearch fails or rejects the document.\n * @throws {InvalidFailureReport} when the report lacks a required field.\n */\nexport async function saveFailureReportToOpenSearch(\n failureReport: FailureReport,\n options: SaveOptions = {},\n): Promise<void> {\n logger.info('Saving failure report to OpenSearch');\n const parsedSample = getKey(failureReport, 'parsedSample');\n const sampleDateValue = getKey(parsedSample, 'date');\n let sampleDate: PyDateTime | null = null;\n if (sampleDateValue !== null && sampleDateValue !== undefined) {\n sampleDate = humanTimestampToDatetime(String(sampleDateValue));\n }\n const originalHeaders = asRecord(getKey(parsedSample, 'headers'));\n const headers: Record<string, unknown> = {};\n for (const [name, value] of Object.entries(originalHeaders)) {\n // The cluster reads a dot in a field name as a nested object, so that\n // `Subject.x` would make `sample.headers.subject` an object in the index.\n setOwn(headers, name.toLowerCase().replaceAll('.', '_'), value);\n }\n\n // arrival_date_utc is a UTC wall-clock string; without assumeUtc the\n // epoch below would be read as local time and skewed by the host's offset.\n const arrivalDateUtc = String(getKey(failureReport, 'arrivalDateUtc'));\n const arrivalDate = humanTimestampToDatetime(arrivalDateUtc, {\n assumeUtc: true,\n });\n const arrivalDateEpochMilliseconds = Math.trunc(\n arrivalDate.timestamp() * MS_PER_SECOND,\n );\n\n const searchIndex = [\n searchPattern('dmarc_failure', options),\n searchPattern('dmarc_forensic', options),\n ].join(',');\n const queries: Query[] = [\n { match: { arrival_date: arrivalDateEpochMilliseconds } },\n ];\n\n let from: Record<string, unknown> | null = null;\n let to: Record<string, unknown> | null = null;\n let subject: unknown = null;\n if (Object.hasOwn(headers, 'from')) {\n // Convert the From header from a list of [name, address] to a string.\n headers.from = flattenAddressHeader(headers.from);\n if (headers.from !== null) {\n from = { 'sample.headers.from': headers.from };\n queries.push({ match_phrase: from });\n }\n }\n if (Object.hasOwn(headers, 'to')) {\n headers.to = flattenAddressHeader(headers.to);\n if (headers.to !== null) {\n to = { 'sample.headers.to': headers.to };\n queries.push({ match_phrase: to });\n }\n }\n if (Object.hasOwn(headers, 'reply-to')) {\n // Flattened like From/To so it can be displayed and aggregated; not\n // part of the dedup query.\n headers['reply-to'] = flattenAddressHeader(headers['reply-to']);\n }\n if (Object.hasOwn(headers, 'subject')) {\n subject = headers.subject;\n queries.push({ match_phrase: { 'sample.headers.subject': subject } });\n }\n\n let existing: number;\n try {\n existing = await countExisting(options, searchIndex, andQueries(queries));\n } catch (error) {\n throw new OpenSearchError(\n `OpenSearch's search for existing report error: ${errorMessage(error)}`,\n );\n }\n if (existing > 0) {\n throw new AlreadySaved(\n `A failure sample to ${pyStr(to)} from ${pyStr(from)} ` +\n `with a subject of ${pyStr(subject)} and arrival date of ${arrivalDateUtc} ` +\n 'already exists in ' +\n 'OpenSearch',\n );\n }\n\n const sample: Record<string, unknown> = {\n raw: getKey(failureReport, 'sample'),\n headers,\n headers_only: getKey(failureReport, 'sampleHeadersOnly'),\n date: sampleDate,\n subject: getKey(parsedSample, 'subject'),\n filename_safe_subject: getKey(parsedSample, 'filenameSafeSubject'),\n body: getKey(parsedSample, 'body'),\n };\n const addressLists = [\n ['to', 'to'],\n ['replyTo', 'reply_to'],\n ['cc', 'cc'],\n ['bcc', 'bcc'],\n ] as const;\n for (const [key, field] of addressLists) {\n for (const address of getKey(parsedSample, key) as unknown[]) {\n appendTo(sample, field, {\n display_name: getKey(address, 'displayName'),\n address: getKey(address, 'address'),\n });\n }\n }\n for (const attachment of getKey(parsedSample, 'attachments') as unknown[]) {\n appendTo(sample, 'attachments', {\n filename: getKey(attachment, 'filename'),\n content_type: getKey(attachment, 'mailContentType'),\n sha256: getKey(attachment, 'sha256'),\n });\n }\n\n try {\n const source = getKey(failureReport, 'source');\n const failureDoc: Record<string, unknown> = {\n feedback_type: getKey(failureReport, 'feedbackType'),\n user_agent: getKey(failureReport, 'userAgent'),\n version: getKey(failureReport, 'version'),\n original_mail_from: getKey(failureReport, 'originalMailFrom'),\n arrival_date: arrivalDateEpochMilliseconds,\n domain: getKey(failureReport, 'reportedDomain'),\n original_envelope_id: getKey(failureReport, 'originalEnvelopeId'),\n authentication_results: getKey(failureReport, 'authenticationResults'),\n delivery_results: getKey(failureReport, 'deliveryResult'),\n source_ip_address: getKey(source, 'ipAddress'),\n source_country: getKey(source, 'country'),\n source_reverse_dns: getKey(source, 'reverseDns'),\n source_base_domain: getKey(source, 'baseDomain'),\n source_asn: getKey(source, 'asn'),\n source_as_name: getKey(source, 'asName'),\n source_as_domain: getKey(source, 'asDomain'),\n authentication_mechanisms: getKey(\n failureReport,\n 'authenticationMechanisms',\n ),\n auth_failure: getKey(failureReport, 'authFailure'),\n dkim_domain: getKey(failureReport, 'dkimDomain'),\n original_rcpt_to: getKey(failureReport, 'originalRcptTo'),\n sample,\n [TIMESTAMP_FIELD]: arrivalDate.isoformat(),\n };\n\n let index = undatedIndexName('dmarc_failure', options);\n if (options.useDataStreams) {\n await createDataStreams(\n [index],\n indexSettings(options),\n INDEX_MAPPINGS.failure,\n options,\n );\n } else {\n index = `${index}-${indexDate(arrivalDate, options.monthlyIndexes)}`;\n await createIndexes(\n [index],\n indexSettings(options),\n INDEX_MAPPINGS.failure,\n options,\n );\n }\n try {\n await saveDocument(options, FAILURE_REPORT_DOC, index, failureDoc);\n } catch (error) {\n throw new OpenSearchError(`OpenSearch error: ${errorMessage(error)}`);\n }\n } catch (error) {\n if (error instanceof KeyError) {\n throw new InvalidFailureReport(\n `Failure report missing required field: ${error.message}`,\n );\n }\n throw error;\n }\n}\n\n/**\n * Saves a parsed SMTP TLS report to OpenSearch.\n *\n * @throws {AlreadySaved} when the report is already in OpenSearch.\n * @throws {OpenSearchError} when OpenSearch fails or rejects the document.\n */\nexport async function saveSmtpTlsReportToOpenSearch(\n report: SMTPTLSReport,\n options: SaveOptions = {},\n): Promise<void> {\n logger.info('Saving SMTP TLS report to OpenSearch');\n const orgName = report.organizationName;\n const { reportId } = report;\n const beginDate = humanTimestampToDatetime(report.beginDate, { toUtc: true });\n const endDate = humanTimestampToDatetime(report.endDate, { toUtc: true });\n const beginDateHuman = beginDate.strftime('%Y-%m-%d %H:%M:%SZ');\n const endDateHuman = endDate.strftime('%Y-%m-%d %H:%M:%SZ');\n\n const query = andQueries([\n { match_phrase: { org_name: orgName } },\n { match_phrase: { report_id: reportId } },\n { match: { date_begin: beginDate.isoformat() } },\n { match: { date_end: endDate.isoformat() } },\n ]);\n const searchIndex = searchPattern('smtp_tls', options);\n\n let existing: number;\n try {\n existing = await countExisting(options, searchIndex, query);\n } catch (error) {\n throw new OpenSearchError(\n `OpenSearch's search for existing report error: ${errorMessage(error)}`,\n );\n }\n if (existing > 0) {\n throw new AlreadySaved(\n `An SMTP TLS report ID ${reportId} from ` +\n ` ${orgName} with a date range of ` +\n `${beginDateHuman} UTC to ` +\n `${endDateHuman} UTC already ` +\n 'exists in OpenSearch',\n );\n }\n\n let index = undatedIndexName('smtp_tls', options);\n if (!options.useDataStreams) {\n index = `${index}-${indexDate(beginDate, options.monthlyIndexes)}`;\n }\n // `org_name` (not the declared `organization_name`) is what parsedmarc\n // stores, and what the dashboards query.\n const smtpTlsDoc: Record<string, unknown> = {\n org_name: report.organizationName,\n date_range: [beginDate, endDate],\n date_begin: beginDate,\n date_end: endDate,\n contact_info: report.contactInfo,\n report_id: report.reportId,\n [TIMESTAMP_FIELD]: beginDate.isoformat(),\n };\n\n for (const policy of report.policies) {\n const policyStrings =\n 'policyStrings' in policy ? policy.policyStrings : null;\n const mxHostPatterns =\n 'mxHostPatterns' in policy ? policy.mxHostPatterns : null;\n // One composed string per policy and per failure detail: dashboards\n // cannot terms-aggregate the subfields of an object array without a\n // cross-product (parsedmarc issue #169).\n const policyDomainCombined = policy.policyDomain || 'none';\n const policyTypeCombined = policy.policyType || 'none';\n appendTo(\n smtpTlsDoc,\n 'policies_combined',\n `${policyDomainCombined} / ${policyTypeCombined}`,\n );\n // `policy_string` (singular) is the key parsedmarc writes.\n const policyDoc: Record<string, unknown> = {\n policy_domain: policy.policyDomain,\n policy_type: policy.policyType,\n successful_session_count: policy.successfulSessionCount,\n failed_session_count: policy.failedSessionCount,\n policy_string: policyStrings ?? null,\n mx_host_patterns: mxHostPatterns ?? null,\n };\n if ('failureDetails' in policy) {\n for (const detail of policy.failureDetails ?? []) {\n // The parser's key is additional_info_uri; the long form is\n // accepted too for reports built by other callers.\n const longForm = detail as { additionalInformationUri?: string };\n let additionalInformationUri: unknown = null;\n if ('additionalInfoUri' in detail) {\n additionalInformationUri = detail.additionalInfoUri;\n } else if ('additionalInformationUri' in detail) {\n additionalInformationUri = longForm.additionalInformationUri;\n }\n const pick = (key: keyof typeof detail): unknown =>\n key in detail ? detail[key] : null;\n const sendingMtaIp = pick('sendingMtaIp');\n const receivingIp = pick('receivingIp');\n const receivingMxHostname = pick('receivingMxHostname');\n appendTo(policyDoc, 'failure_details', {\n result_type: detail.resultType,\n ip_address: pick('ipAddress'),\n sending_mta_ip: ipOrNull(sendingMtaIp, 'sending_mta_ip'),\n receiving_mx_hostname: receivingMxHostname,\n receiving_mx_helo: pick('receivingMxHelo'),\n receiving_ip: ipOrNull(receivingIp, 'receiving_ip'),\n failed_session_count: detail.failedSessionCount,\n additional_information_uri: additionalInformationUri,\n failure_reason_code: pick('failureReasonCode'),\n });\n appendTo(\n smtpTlsDoc,\n 'failure_details_combined',\n [\n policyDomainCombined,\n policyTypeCombined,\n detail.resultType || 'none',\n pyStr(sendingMtaIp || 'none'),\n pyStr(receivingIp || 'none'),\n pyStr(receivingMxHostname || 'none'),\n ].join(' / '),\n );\n }\n }\n appendTo(smtpTlsDoc, 'policies', policyDoc);\n }\n\n if (options.useDataStreams) {\n await createDataStreams(\n [index],\n indexSettings(options),\n INDEX_MAPPINGS.smtpTls,\n options,\n );\n } else {\n await createIndexes(\n [index],\n indexSettings(options),\n INDEX_MAPPINGS.smtpTls,\n options,\n );\n }\n\n try {\n await saveDocument(options, SMTP_TLS_REPORT_DOC, index, smtpTlsDoc);\n } catch (error) {\n throw new OpenSearchError(`OpenSearch error: ${errorMessage(error)}`);\n }\n}\n\n/** Backward-compatible alias of {@link saveFailureReportToOpenSearch}. */\nexport const saveForensicReportToOpenSearch: typeof saveFailureReportToOpenSearch =\n saveFailureReportToOpenSearch;\n"],"mappings":";;;;AASA,MAAM,YAAY;;AAGlB,IAAa,WAAb,cAA8B,MAAM;CAClC,OAAgB;CAChB;CAEA,YAAY,KAAa;EAEvB,MAAM,OAAO,GAAG,CAAC;EACjB,KAAK,MAAM;CACb;AACF;;;;;;AAOA,SAAgB,OACd,QACA,KACA,OACM;CACN,OAAO,eAAe,QAAQ,KAAK;EACjC,cAAc;EACd,YAAY;EACZ;EACA,UAAU;CACZ,CAAC;AACH;;AAGA,IAAa,aAAb,cAAgC,MAAM;CACpC,OAAgB;AAClB;AAEA,SAASA,WAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;;AAOA,SAAgB,OAAO,KAAc,KAAsB;CACzD,IAAI,CAACA,WAAS,GAAG,KAAK,EAAE,OAAO,MAC7B,MAAM,IAAI,SAAS,aAAa,GAAG,CAAC;CAEtC,OAAO,IAAI;AACb;;AAGA,SAAgB,OAAO,OAAyB;CAC9C,IAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,OACrD,OAAO;CAET,IAAI,OAAO,UAAU,UACnB,OAAO,UAAU,KAAK,CAAC,OAAO,MAAM,KAAK;CAE3C,IAAI,OAAO,UAAU,UACnB,OAAO,UAAU;CAEnB,IAAI,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAClD,OAAO,MAAM,SAAS;CAExB,IAAIA,WAAS,KAAK,GAChB,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS;CAErC,OAAO;AACT;;AAGA,SAAS,SAAS,OAAgC;CAChD,OAAO,SAAS,OAAO,OAAO,gBAAgB,KAC5C,SAAS,OAAO,OAAO,gBAAgB,IACrC,OAAO,KAAK,IACZ;AACN;;;;;AAMA,SAAgB,MAAM,OAAiC;CACrD,IAAI,OAAO,UAAU,WACnB,OAAO,QAAQ,IAAI;CAErB,IAAI,OAAO,UAAU,UACnB,OAAO,SAAS,KAAK;CAEvB,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,KAAK,GACxB,MAAM,IAAI,WACR,wBAAwB,OAAO,MAAM,KAAK,IAAI,QAAQ,WAAW,YACnE;EAEF,OAAO,SAAS,OAAO,KAAK,MAAM,KAAK,CAAC,CAAC;CAC3C;CACA,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,OAAO,MAAM,KAAK;EACxB,IAAI,UAAU,KAAK,IAAI,GACrB,OAAO,SAAS,OAAO,KAAK,WAAW,KAAK,EAAE,CAAC,CAAC;EAElD,MAAM,IAAI,WACR,2CAA2C,OAAO,KAAK,GACzD;CACF;CACA,MAAM,IAAI,WACR,+EAA+E,OAAO,MAAM,EAC9F;AACF;;AAsCA,SAAgB,aAAa,OAAwB;CACnD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;;ACnJA,MAAM,eAAe;AACrB,MAAM,kBAAkB;AACxB,MAAM,aAAa;AACnB,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;;AAG3B,SAAS,UAAU,MAA+B;CAChD,IAAI,SAAS,MAAM,KAAK,SAAS,GAAG,GAClC,OAAO;CAET,MAAM,SAAS,KAAK,MAAM,GAAG;CAC7B,IAAI,OAAO,WAAW,GACpB,OAAO;CAET,MAAM,SAAmB,CAAC;CAC1B,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,CAAC,eAAe,KAAK,KAAK,KAAK,MAAM,SAAS,GAChD,OAAO;EAGT,IAAI,UAAU,OAAO,MAAM,WAAW,GAAG,GACvC,OAAO;EAET,MAAM,QAAQ,OAAO,KAAK;EAC1B,IAAI,QAAQ,KACV,OAAO;EAET,OAAO,KAAK,KAAK;CACnB;CACA,OAAO;AACT;AAEA,SAAS,YAAY,MAA6B;CAChD,IAAI,SAAS,MAAM,CAAC,WAAW,KAAK,IAAI,KAAK,KAAK,SAAS,GACzD,OAAO;CAET,OAAO,OAAO,SAAS,MAAM,EAAE;AACjC;;AAGA,SAAS,iBAAiB,MAA+B;CACvD,IAAI,SAAS,MAAM,KAAK,SAAS,iBAC/B,OAAO;CAET,MAAM,WAAW;CACjB,MAAM,QAAQ,KAAK,MAAM,GAAG;CAC5B,IAAI,MAAM,SAAS,IACjB,OAAO;CAET,IAAI,MAAM,SAAS,GACjB,OAAO;CAET,MAAM,OAAO,MAAM,GAAG,EAAE,KAAK;CAC7B,IAAI,KAAK,SAAS,GAAG,GAAG;EACtB,MAAM,OAAO,UAAU,IAAI;EAC3B,IAAI,SAAS,MACX,OAAO;EAET,MAAM,IAAI;EACV,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK;EACrC,MAAM,MAAO,KAAK,IAAK,EAAA,CAAG,SAAS,EAAE,IAAK,KAAK,IAAK,EAAA,CAAG,SAAS,EAAE,CAAC;CACrE;CACA,IAAI,MAAM,SAAS,UACjB,OAAO;CAET,IAAI,YAA2B;CAC/B,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,SAAS,GAAG,SAC5C,IAAI,MAAM,WAAW,IAAI;EACvB,IAAI,cAAc,MAChB,OAAO;EAET,YAAY;CACd;CAEF,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI,cAAc,MAAM;EACtB,IAAI,MAAM,WAAW,gBAAgB,MAAM,OAAO,MAAM,SAAS,IAC/D,OAAO;EAET,UAAU,MAAM;EAChB,UAAU;EACV,eAAe;CACjB,OAAO;EACL,UAAU;EACV,UAAU,MAAM,SAAS,YAAY;EACrC,IAAI,MAAM,OAAO,IAAI;GACnB,WAAW;GACX,IAAI,YAAY,GACd,OAAO;EAEX;EACA,IAAI,MAAM,GAAG,EAAE,MAAM,IAAI;GACvB,WAAW;GACX,IAAI,YAAY,GACd,OAAO;EAEX;EACA,eAAe,gBAAgB,UAAU;EACzC,IAAI,eAAe,GACjB,OAAO;CAEX;CACA,MAAM,UAAoB,CAAC;CAC3B,KAAK,IAAI,QAAQ,GAAG,QAAQ,SAAS,SAAS;EAC5C,MAAM,QAAQ,YAAY,MAAM,UAAU,EAAE;EAC5C,IAAI,UAAU,MACZ,OAAO;EAET,QAAQ,KAAK,KAAK;CACpB;CACA,KAAK,IAAI,QAAQ,GAAG,QAAQ,cAAc,SACxC,QAAQ,KAAK,CAAC;CAEhB,KAAK,IAAI,QAAQ,MAAM,SAAS,SAAS,QAAQ,MAAM,QAAQ,SAAS;EACtE,MAAM,QAAQ,YAAY,MAAM,UAAU,EAAE;EAC5C,IAAI,UAAU,MACZ,OAAO;EAET,QAAQ,KAAK,KAAK;CACpB;CACA,OAAO;AACT;;AAGA,SAAS,gBAAgB,SAAoC;CAC3D,IAAI,YAAY;CAChB,IAAI,aAAa;CACjB,IAAI,QAAQ;CACZ,IAAI,SAAS;CACb,QAAQ,SAAS,QAAQ,UAAU;EACjC,IAAI,WAAW,GAAG;GAChB,UAAU;GACV,IAAI,UAAU,IACZ,QAAQ;GAEV,IAAI,SAAS,YAAY;IACvB,aAAa;IACb,YAAY;GACd;EACF,OAAO;GACL,SAAS;GACT,QAAQ;EACV;CACF,CAAC;CACD,MAAM,QAAQ,QAAQ,KAAI,WAAU,OAAO,SAAS,EAAE,CAAC;CACvD,IAAI,aAAa,GAAG;EAClB,MAAM,MAAM,YAAY;EACxB,MAAM,OAAO,MAAM,MAAM,GAAG,SAAS;EACrC,MAAM,OAAO,MAAM,MAAM,GAAG;EAC5B,OAAO,GAAG,KAAK,KAAK,GAAG,EAAE,IAAI,KAAK,KAAK,GAAG;CAC5C;CACA,OAAO,MAAM,KAAK,GAAG;AACvB;AAEA,SAAS,WAAW,MAA6B;CAC/C,IAAI,KAAK,SAAS,GAAG,GACnB,OAAO;CAET,MAAM,UAAU,KAAK,QAAQ,GAAG;CAChC,IAAI,UAAU;CACd,IAAI,UAAyB;CAC7B,IAAI,YAAY,IAAI;EAClB,UAAU,KAAK,MAAM,UAAU,CAAC;EAChC,UAAU,KAAK,MAAM,GAAG,OAAO;EAC/B,IAAI,YAAY,MAAM,QAAQ,SAAS,GAAG,GACxC,OAAO;CAEX;CACA,MAAM,UAAU,iBAAiB,OAAO;CACxC,IAAI,YAAY,MACd,OAAO;CAET,MAAM,eACJ,QAAQ,MAAM,GAAG,CAAC,CAAC,CAAC,OAAM,WAAU,WAAW,CAAC,KAChD,QAAQ,OAAO;CACjB,MAAM,CAAC,OAAO,GAAG,MAAM,KAAK,QAAQ,MAAM,CAAC;CAC3C,MAAM,YAAY,eACd,UAAU,QAAQ,EAAE,GAAG,OAAO,IAAK,GAAG,OAAO,EAAE,GAAG,MAAM,QACxD,gBAAgB,OAAO;CAC3B,OAAO,UAAU,GAAG,UAAU,GAAG,YAAY;AAC/C;;;;;;AAOA,SAAgB,mBAAmB,OAAuB;CACxD,MAAM,OAAO,UAAU,KAAK;CAC5B,IAAI,SAAS,MACX,OAAO,KAAK,KAAK,GAAG;CAEtB,MAAM,OAAO,WAAW,KAAK;CAC7B,IAAI,SAAS,MACX,OAAO;CAET,MAAM,IAAI,WACR,GAAG,OAAO,KAAK,EAAE,+CACnB;AACF;;ACtJA,MAAMC,kBAAgB;AAEtB,SAAS,cAAc,OAAoC;CACzD,OAAO,MAAM,SAAS,YAAY,MAAM,SAAS;AACnD;AAEA,SAAS,cAAc,OAAkD;CACvE,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO;CAET,MAAM,YAAqB,OAAO,eAAe,KAAK;CACtD,OAAO,cAAc,OAAO,aAAa,cAAc;AACzD;;AAGA,SAAgB,aAAa,OAAyB;CACpD,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;CAET,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,WAAW;CAE1B,OAAO,cAAc,KAAK,KAAK,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW;AAC/D;;AAGA,SAAS,cAAc,OAAwB;CAC7C,IAAI,iBAAiB,YACnB,OAAO,MAAM,UAAU;CAEzB,IAAI,OAAO,UAAU,UACnB,OAAO,UAAU,KAAK,CAAC,CAAC,UAAU;CAEpC,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,GAErD,OAAO,WAAW,cAAc,QAAQA,iBAAe,IAAI,CAAC,CACzD,WAAW,IAAI,CAAC,CAChB,UAAU;CAEf,MAAM,IAAI,WACR,wCAAwC,OAAO,KAAK,EAAE,EACxD;AACF;;;;;;;AAQA,SAAgB,UAAU,OAAyB;CACjD,OAAO,cAAc,KAAK,IAAI,MAAM,KAAK,IAAI;AAC/C;;;;;;AAOA,SAAS,YACP,OACA,OACA,OACS;CACT,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;CAET,IAAI;EACF,MAAM,KAAK;EACX,OAAO;CACT,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,aACrB,MAAM;EAER,OAAO,QACL,YAAY,MAAM,kBAAkB,eAAe,aAAa,KAAK,CAAC,GACxE;EACA,OAAO;CACT;AACF;;AAGA,SAAgB,SAAS,OAAgB,OAAwB;CAC/D,OAAO,YAAY,OAAO,QAAO,SAAQ,mBAAmB,OAAO,IAAI,CAAC,CAAC;AAC3E;;AAGA,SAAgB,cAAc,OAAgB,OAAwB;CACpE,OAAO,YAAY,OAAO,OAAO,KAAK;AACxC;AAEA,SAAS,gBAAgB,OAAoB,OAAyB;CACpE,QAAQ,MAAM,MAAd;EACE,KAAK,WACH,OAAO,MAAM,KAAK;EACpB,KAAK,WACH,OAAO,UAAU,UAAU,QAAQ,OAAO,KAAK;EACjD,KAAK,QACH,OAAO,cAAc,KAAK;EAC5B,KAAK,MACH,OAAO,mBAAmB,OAAO,KAAK,CAAC;EACzC,SACE,OAAO,UAAU,KAAK;CAC1B;AACF;AAEA,SAAS,aAAa,OAAc,OAAyB;CAC3D,IAAI,cAAc,KAAK,GAGrB,OAAO,cAAc,KAAK,IACtB,kBAAkB,MAAM,cAAc,CAAC,GAAG,KAAK,IAC/C;CAEN,OAAO,gBAAgB,OAAO,KAAK;AACrC;AAEA,SAAS,eAAe,OAAc,OAAyB;CAC7D,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAI,SACf,SAAS,QAAQ,SAAS,KAAA,IAAY,OAAO,aAAa,OAAO,IAAI,CACvE;CAEF,OAAO,aAAa,OAAO,KAAK;AAClC;;;;;;;;AASA,SAAgB,kBACd,QACA,MACyB;CACzB,MAAM,MAA+B,CAAC;CACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAAG;EAC/C,MAAM,QAAQ,OAAO,OAAO,QAAQ,GAAG,IAAI,OAAO,OAAO,KAAA;EACzD,MAAM,aACJ,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,KAAA,IAC/C,QACA,eAAe,OAAO,KAAK;EACjC,IAAI,CAAC,aAAa,UAAU,GAC1B,OAAO,KAAK,KAAK,UAAU;CAE/B;CACA,OAAO;AACT;AAEA,SAAS,aAAa,OAAuC;CAC3D,IAAI,cAAc,KAAK,GAAG;EACxB,IAAI,MAAM,eAAe,KAAA,GACvB,OAAO,EAAE,MAAM,MAAM,KAAK;EAE5B,OAAO;GAAE,GAAG,cAAc,MAAM,UAAU;GAAG,MAAM,MAAM;EAAK;CAChE;CACA,MAAM,UAAmC,EAAE,MAAM,MAAM,KAAK;CAC5D,IAAI,MAAM,WAAW,KAAA,GACnB,QAAQ,SAAS,gBAAgB,MAAM,MAAM;CAE/C,OAAO;AACT;;AAGA,SAAgB,cAAc,QAE5B;CACA,MAAM,aAAsC,CAAC;CAC7C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,GAC/C,WAAW,QAAQ,aAAa,KAAK;CAEvC,OAAO,EAAE,WAAW;AACtB;AAEA,SAAS,gBAAgB,QAAiD;CACxE,MAAM,aAAsC,CAAC;CAC7C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,GAC/C,IAAI,MAAM,SAAS,QACjB,WAAW,QAAQ,EAAE,MAAM,OAAO;MAC7B,IAAI,cAAc,KAAK,KAAK,MAAM,iBAAiB,KAAA,GACxD,WAAW,QAAQ,gBAAgB,MAAM,YAAY;MAChD,IAAI,MAAM,SAAS,YAAY,MAAM,eAAe,KAAA,GAAW;EACpE,MAAM,QAAQ,gBAAgB,MAAM,UAAU;EAC9C,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,GAC9B,WAAW,QAAQ,EAAE,YAAY,MAAM;CAE3C;CAEF,OAAO;AACT;;;;;;AAOA,MAAa,kBAAkB;;;;;AAM/B,MAAa,oBAET,EACF,YAAY,GAAG,kBAAkB,EAAE,MAAM,OAAO,EAAE,EACpD;;;;;;;;;;;AAYA,SAAgB,aAAa,QAAsC;CACjE,OAAO;EACL,gBAAgB;EAChB,YAAY;GAAE,GAAG,kBAAkB;GAAY,GAAG,gBAAgB,MAAM;EAAE;CAC5E;AACF;;;;;;;AAQA,SAAgB,kBACd,UACyB;CACzB,IAAI,aAAa,MACf,OAAO,gBAAgB,iBAAiB;CAE1C,OAAO;EACL,GAAG,gBAAgB,QAAQ;EAC3B,YAAY;GACV,GAAG,gBAAgB,SAAS,UAAU;GACtC,GAAG,gBAAgB,kBAAkB,UAAU;EACjD;CACF;AACF;;;;;AASA,SAAgB,WAAW,SAAkC;CAC3D,MAAM,CAAC,SAAS;CAChB,IAAI,QAAQ,WAAW,KAAK,UAAU,KAAA,GACpC,OAAO;CAET,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,EAAE,EAAE;AACxC;;AAGA,SAAgB,SACd,KACA,KACA,OACM;CACN,MAAM,UAAU,IAAI;CACpB,IAAI,MAAM,QAAQ,OAAO,GACvB,QAAQ,KAAK,KAAK;MAElB,IAAI,OAAO,CAAC,KAAK;AAErB;;ACnUA,MAAM,OAAc,EAAE,MAAM,OAAO;AACnC,MAAM,UAAiB,EAAE,MAAM,UAAU;AACzC,MAAM,UAAiB,EAAE,MAAM,UAAU;AACzC,MAAM,UAAiB,EAAE,MAAM,UAAU;AACzC,MAAM,OAAc,EAAE,MAAM,OAAO;AACnC,MAAM,KAAY,EAAE,MAAM,KAAK;;AAG/B,MAAM,gBAAuB;CAC3B,MAAM;CACN,QAAQ,EAAE,SAAS;EAAE,MAAM;EAAW,cAAc;CAAI,EAAE;AAC5D;;AAgCA,MAAa,uBAAuC;CAClD,YAAY;CACZ,eAAe;CACf,UAAU;CACV,WAAW;CACX,wBAAwB;CACxB,WAAW;CACX,YAAY;CACZ,YAAY;CACZ,UAAU;CACV,qBAAqB;CACrB,QAAQ;CACR,kBAAkB;EAAE,MAAM;EAAU,YAAY;GAvChD,QAAQ;GACR,OAAO;GACP,MAAM;GACN,GAAG;GACH,IAAI;GACJ,KAAK;GACL,IAAI;GACJ,IAAI;GACJ,SAAS;GACT,kBAAkB;EA8B8B;CAAqB;CACrE,mBAAmB;CACnB,gBAAgB;CAChB,oBAAoB;CACpB,oBAAoB;CACpB,aAAa;CACb,aAAa;CACb,YAAY;CACZ,gBAAgB;CAChB,kBAAkB;CAClB,aAAa;CACb,cAAc;CACd,aAAa;CACb,cAAc;CACd,kBAAkB;EAAE,MAAM;EAAU,YAAY;GAxDJ,MAAM;GAAM,SAAS;EAwDjB;CAAoB;CACpE,aAAa;CACb,eAAe;CACf,aAAa;CAIb,cAAc;EAAE,MAAM;EAAU,YAAY;GA/C5C,QAAQ;GACR,UAAU;GACV,QAAQ;GACR,cAAc;EA4C8B;CAAgB;CAC5D,aAAa;EAAE,MAAM;EAAU,YAAY;GAzC3C,QAAQ;GACR,OAAO;GACP,QAAQ;GACR,cAAc;EAsC6B;CAAe;CAC1D,uBAAuB;CACvB,sBAAsB;CACtB,IAAI;CACJ,SAAS;CACT,kBAAkB;CAClB,WAAW;AACb;AAEA,MAAM,oBAAoC;CAAE,cAAc;CAAM,SAAS;AAAK;;AAG9E,MAAa,uBAAuC;CAClD,UAAU;CACV,cAAc;CACd,QAAQ;AACV;;AAsFA,MAAa,qBAAqC;CAChD,eAAe;CACf,YAAY;CACZ,SAAS;CACT,oBAAoB;CACpB,cAAc;CACd,QAAQ;CACR,sBAAsB;CACtB,wBAAwB;CACxB,kBAAkB;CAClB,mBAAmB;CACnB,gBAAgB;CAChB,oBAAoB;CACpB,YAAY;CACZ,gBAAgB;CAChB,kBAAkB;CAClB,kCAAkC;CAClC,sBAAsB;CACtB,aAAa;CACb,kBAAkB;CAClB,QAAQ;EAAE,MAAM;EAAU,YAAY;GArCtC,KAAK;GAEL,SAAS;IAAE,MAAM;IAAU,cAAc;KAhBzC,SAAS;KACT,YAAY,OAAO,YACjB;MA9CF;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;MACA;KAWE,CAAA,CAAuB,KAAI,SAAQ,CACjC,MACA;MACE,MAAM;MACN,QAAQ,EAAE,SAAS;OAAE,MAAM;OAAW,cAAc;MAAI,EAAE;KAC5D,CACF,CAAC,CACH;IAOyC;GAAuB;GAChE,cAAc;GACd,IAAI;IAAE,MAAM;IAAU,YAAY;GAAkB;GACpD,SAAS;GACT,uBAAuB;GACvB,OAAO;IAAE,MAAM;IAAU,YAAY;GAAkB;GACvD,MAAM;GACN,UAAU;IAAE,MAAM;IAAU,YAAY;GAAkB;GAC1D,IAAI;IAAE,MAAM;IAAU,YAAY;GAAkB;GACpD,KAAK;IAAE,MAAM;IAAU,YAAY;GAAkB;GACrD,MAAM;GACN,aAAa;IAAE,MAAM;IAAU,YAAY;GAAqB;EAwB1B;CAAmB;AAC3D;;AAwBA,MAAa,sBAAsC;CACjD,mBAAmB;CACnB,YAAY;CACZ,YAAY;CACZ,UAAU;CACV,cAAc;CACd,WAAW;CACX,UAAU;EAAE,MAAM;EAAU,YAAY;GAjBxC,eAAe;GACf,aAAa;GACb,gBAAgB;GAChB,kBAAkB;GAClB,0BAA0B;GAC1B,sBAAsB;GACtB,iBAAiB;IAAE,MAAM;IAAU,YAAY;KAjB/C,aAAa;KACb,gBAAgB;KAChB,mBAAmB;KACnB,uBAAuB;KACvB,cAAc;KACd,sBAAsB;KACtB,4BAA4B;KAC5B,qBAAqB;IAUqD;GAAE;EAWpC;CAAoB;CAC5D,mBAAmB;CACnB,0BAA0B;AAC5B;;;;;;;AAcA,MAAa,oBAAsC;CACjD,WAAW,cAAc,oBAAoB;CAC7C,SAAS,cAAc,kBAAkB;CACzC,SAAS,cAAc,mBAAmB;AAC5C;;AAGA,MAAa,iBAIT;CACF,WAAW,aAAa,oBAAoB;CAC5C,SAAS,aAAa,kBAAkB;CACxC,SAAS,aAAa,mBAAmB;AAC3C;;;;;;;AC3QA,MAAa,0BAA6C;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;AAOA,MAAM,wBAAwB;AAE9B,SAAS,WAAW,OAAyB;CAC3C,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC9B,OAAO;CAET,IAAI,iBAAiB,YACnB,OAAO,MAAM,UAAU;CAEzB,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,IAAI,UAAU;CAE7B,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAChD,OAAO,OAAO,KAAK;CAErB,IAAI,OAAO,UAAU,UAEnB,OAAO,EAAE,QAAQ,OAAO,KAAK,EAAE;CAEjC,OAAO;AACT;;AAGA,SAAS,YAAY,UAA2B;CAC9C,MAAM,SACJ,OAAO,aAAa,YAAY,aAAa,OACxC,WACD,CAAC;CACP,OAAO,KAAK,UACV,wBAAwB,KAAI,UAC1B,WAAW,OAAO,OAAO,QAAQ,KAAK,IAAI,OAAO,SAAS,IAAI,CAChE,CACF;AACF;;AAGA,MAAM,sBAAsB;;;;;;;;;;;;;;;;;AAkB5B,eAAsB,kBACpB,WACA,OACA,aACqC;CACrC,IACE,cAAc,QACd,MAAM,WAAW,KACjB,MAAM,UAAU,UAAU,UAC1B,UAAU,SAAS,uBAEnB,OAAO;CAET,MAAM,SACJ,MAAM,SAAS,sBACX,QACA,MAAM,YAAY,UAAU,SAAS,CAAC;CAC5C,IAAI,OAAO,WAAW,KAAK,OAAO,UAAU,UAAU,QACpD,OAAO;CAET,MAAM,4BAAY,IAAI,IAAsB;CAC5C,KAAK,MAAM,CAAC,UAAU,aAAa,UAAU,QAAQ,GAAG;EACtD,MAAM,MAAM,YAAY,QAAQ;EAChC,MAAM,OAAO,UAAU,IAAI,GAAG;EAC9B,IAAI,SAAS,KAAA,GACX,UAAU,IAAI,KAAK,CAAC,QAAQ,CAAC;OAE7B,KAAK,KAAK,QAAQ;CAEtB;CACA,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,UAAU,QAAQ;EAC3B,MAAM,WAAW,UAAU,IAAI,YAAY,MAAM,CAAC,CAAC,EAAE,MAAM;EAC3D,IAAI,aAAa,KAAA,GACf,OAAO;EAET,MAAM,IAAI,QAAQ;CACpB;CACA,OAAO;AACT;;ACvHA,MAAM,iBAAiB;AACvB,MAAM,YAAY;AAClB,MAAM,kBAAkB;AACxB,MAAM,uBAAuB;AAC7B,MAAM,iBAAiB;AAEvB,MAAM,mBAAmB;CAAC;CAAO;CAAU;AAAU;;AAGrD,IAAa,YAAb,cAA+B,MAAM;CACnC,OAAgB;CAChB;CAEA,YAAY,UAAkB,SAAiB;EAC7C,MAAM,OAAO;EACb,KAAK,WAAW;CAClB;AACF;;AAGA,IAAa,iBAAb,cAAoC,MAAM;CACxC,OAAgB;CAChB;CAEA,YAAY,SAAiB,QAAmB;EAC9C,MAAM,OAAO;EACb,KAAK,SAAS;CAChB;AACF;;;;;;AAyBA,SAAS,gBAAgB,UAA0B,UAAwB;CACzE,MAAM,SAAS,SAAS;CACxB,IAAI,QAAQ;EACV,MAAM,aAAa,OAAO,cAAc;EACxC,MAAM,UAAU,OAAO,WAAW;EAClC,MAAM,QAAQ,OAAO,SAAS;EAC9B,IAAI,aAAa,UAAU,OACzB,MAAM,IAAI,UACR,UACA,wCAAwC,WAAW,KAAK,QAAQ,0BAA0B,MAAM,EAClG;CAEJ;AACF;AAEA,gBAAgB,KACd,QACA,OACA,QACqB;CACrB,IAAI,YACF,MAAM,OAAO,OAAO;EAClB;EACA;EACA,MAAM;EACN,MAAM,EAAE,MAAM,OAAO;CACvB,CAAC,EAAA,CACD;CACF,IAAI,WAAW,SAAS;CACxB,IAAI;EACF,OAAO,UAAU;GACf,MAAM,OAAO,SAAS,MAAM,QAAQ,CAAC;GACrC,OAAO;GACP,gBAAgB,UAAU,QAAQ;GAClC,IAAI,KAAK,WAAW,GAClB;GAEF,YACE,MAAM,OAAO,OAAO,EAAE,MAAM;IAAE,WAAW;IAAU;GAAO,EAAE,CAAC,EAAA,CAC7D;GACF,WAAW,SAAS;EACtB;CACF,UAAU;EACR,IAAI,UACF,MAAM,OAAO,YACX,EAAE,MAAM,EAAE,WAAW,CAAC,QAAQ,EAAE,EAAE,GAClC,EAAE,QAAQ,CAAC,cAAc,EAAE,CAC7B;CAEJ;AACF;;AAGA,SAAS,UAAU,KAA8C;CAC/D,MAAM,OAAgC,CAAC;CACvC,KAAK,MAAM,OAAO,kBAChB,IAAI,OAAO,KACT,KAAK,OAAO,IAAI;CAIpB,OAAO,CACL,EAAE,OAAO,KAAK,GACd,gBAAgB,aAAa,MAAM,IAAI,UAAU,GAAG,CACtD;AACF;AAEA,eAAe,UAAU,QAAgB,MAAgC;CACvE,MAAM,YAAY,MAAM,OAAO,KAAK,EAAQ,KAAgB,CAAC,EAAA,CAC1D;CACH,IAAI,CAAC,SAAS,OACZ,MAAM,IAAI,eAAe,mCAAmC,CAAC,CAAC;CAEhE,MAAM,SAAoB,CAAC;CAC3B,KAAK,MAAM,QAAQ,SAAS,OAC1B,KAAK,MAAM,CAAC,QAAQ,WAAW,OAAO,QAAQ,IAAI,GAAG;EACnD,MAAM,SAAS,QAAQ,UAAU;EACjC,IAAI,SAAS,OAAO,UAAU,KAC5B,OAAO,KAAK,GAAG,SAAS,OAAO,CAAC;CAEpC;CAEF,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,eACR,GAAG,OAAO,OAAO,gCACjB,MACF;AAEJ;;;;;AAMA,eAAsB,QACpB,QACA,aACA,aACe;CACf,IAAI,OAAkB,CAAC;CACvB,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,WAAW,MAAM,OAAO,KAAK,QAAQ,aAAa,cAAc,GAAG;EACjE,MAAM,CAAC,QAAQ,YAAY,UAAU;GAAE,GAAG;GAAK,QAAQ;EAAY,CAAC;EACpE,MAAM,OACJ,OAAO,WAAW,KAAK,UAAU,MAAM,CAAC,IACxC,OAAO,WAAW,KAAK,UAAU,QAAQ,CAAC,IAC1C;EACF,IACE,QAAQ,MACP,UAAU,mBAAmB,QAAQ,OAAO,uBAC7C;GACA,MAAM,UAAU,QAAQ,IAAI;GAC5B,OAAO,CAAC;GACR,QAAQ;GACR,QAAQ;EACV;EACA,KAAK,KAAK,QAAQ,QAAQ;EAC1B,SAAS;EACT,SAAS;CACX;CACA,IAAI,QAAQ,GACV,MAAM,UAAU,QAAQ,IAAI;AAEhC;;;AChKA,MAAM,kCAAuC,IAAI,IAAI;CAAC;CAAK;CAAK;AAAG,CAAC;AAEpE,SAAS,SAAS,OAAyB;CACzC,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA6B,SAAS;AAElD;;AAGA,SAAgB,YAAY,OAAyB;CACnD,IAAI,iBAAiB,OAAO,cAC1B,OAAO;CAET,IAAI,iBAAiB,OAAO,iBAC1B,OAAO;CAET,OACE,iBAAiB,OAAO,iBACxB,gBAAgB,IAAI,MAAM,UAAU;AAExC;;AAGA,eAAe,QACb,SACA,UACe;CACf,IAAI;CACJ,IAAI;EACF,SAAS,MAAM;CACjB,SAAS,OAAO;EACd,SAAS,OAAoB,MAAgC,IAAI;EACjE;CACF;CAEA,SAAS,MAA6B,MAAM;AAC9C;;;;;AAMA,SAAgB,kBAAkB,MAAsC;CACtE,MAAM,6BAA6B,KAAK;EACtC,QACE,QACA,UAAqD,CAAC,GACtD,UACkB;GAClB,IAAI,iBAA0C,CAAC;GAC/C,IAAI,OAAO;GACX,IAAI,OAAO,YAAY,YACrB,OAAO;QAEP,iBAAiB;GAGnB,MAAM,aACJ,SAAS,OAAO,IAAI,KAAK,SAAS,OAAO,QAAQ,IAC7C,IACC,eAAe,cAAc,KAAK;GACzC,IAAI,UAAwC;GAC5C,IAAI,UAAU;GACd,MAAM,gBAAsC;IAC1C,MAAM,UAAU,MAAM,QAAQ,QAAQ;KACpC,GAAG;KACH,YAAY;IACd,CAAC;IACD,UAAU;IACV,OAAO;GACT;GACA,MAAM,MAAM,YAAkC;IAC5C,KAAK,IAAI,UAAU,IAAK,WAAW,GACjC,IAAI;KAEF,OAAO,MAAM,QAAQ;IACvB,SAAS,OAAO;KACd,IAAI,WAAW,WAAW,cAAc,CAAC,YAAY,KAAK,GACxD,MAAM;IAEV;GAEJ;GACA,MAAM,UAAU,IAAI;GACpB,MAAM,cAAoB;IACxB,UAAU;IACV,SAAS,MAAM;GACjB;GACA,IAAI,MACF,QAAQ,SAAS,IAAI,CAAC,CAAC,OAAO,UAAmB;IAG/C,qBAAqB;KACnB,MAAM;IACR,CAAC;GACH,CAAC;GAEH,OAAO,OAAO,OAAO,SAAS,EAAE,MAAM,CAAC;EACzC;CACF;CACA,OAAO;AACT;;;AC9DA,IAAa,kBAAb,cAAqC,MAAM;CACzC,OAAgB;AAClB;;AAGA,IAAa,eAAb,cAAkC,MAAM;CACtC,OAAgB;AAClB;AAEA,MAAM,gBAAgB;AACtB,MAAM,iCAAiC;AACvC,MAAM,0BAA0B;AAChC,MAAM,eAAe;AACrB,MAAM,aAAa;AAanB,IAAI,oBAAmC;AAOvC,MAAM,gCAAgC;AAqBtC,MAAM,mCAAmB,IAAI,QAAiC;AAU9D,MAAM,0BAAiC,EACrC,MAAM;CACJ,sBAAsB;CACtB,QAAQ,CACN,EACE,MAAM;EACJ,MAAM,CACJ,EACE,MAAM;GACJ,sBAAsB;GACtB,QAAQ,CACN,EAAE,QAAQ,EAAE,OAAO,sBAAsB,EAAE,GAC3C,EAAE,QAAQ,EAAE,OAAO,sBAAsB,EAAE,CAC7C;EACF,EACF,CACF;EACA,UAAU,CAAC,EAAE,QAAQ,EAAE,OAAO,wBAAwB,EAAE,CAAC;CAC3D,EACF,GACA,EACE,MAAM;EACJ,MAAM,CACJ,EACE,MAAM;GACJ,sBAAsB;GACtB,QAAQ,CACN,EAAE,QAAQ,EAAE,OAAO,qBAAqB,EAAE,GAC1C,EAAE,QAAQ,EAAE,OAAO,qBAAqB,EAAE,CAC5C;EACF,EACF,CACF;EACA,UAAU,CAAC,EAAE,QAAQ,EAAE,OAAO,uBAAuB,EAAE,CAAC;CAC1D,EACF,CACF;AACF,EACF;AAMA,MAAM,2BAA2B;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,EAAE;AAIT,MAAM,mCAA0C,EAC9C,MAAM;CACJ,sBAAsB;CACtB,QAAQ,CACN,EACE,MAAM;EACJ,MAAM,CACJ,EACE,MAAM;GACJ,sBAAsB;GACtB,QAAQ,CACN,EAAE,QAAQ,EAAE,OAAO,yBAAyB,EAAE,GAC9C,EAAE,QAAQ,EAAE,OAAO,uBAAuB,EAAE,CAC9C;EACF,EACF,CACF;EACA,UAAU,CAAC,EAAE,QAAQ,EAAE,OAAO,oBAAoB,EAAE,CAAC;CACvD,EACF,GACA,EACE,MAAM;EACJ,MAAM,CACJ,EACE,MAAM;GACJ,sBAAsB;GACtB,QAAQ,CACN,EACE,QAAQ,EAAE,OAAO,uCAAuC,EAC1D,GACA,EACE,QAAQ,EACN,OAAO,0CACT,EACF,CACF;EACF,EACF,CACF;EACA,UAAU,CAAC,EAAE,QAAQ,EAAE,OAAO,2BAA2B,EAAE,CAAC;CAC9D,EACF,CACF;AACF,EACF;AAKA,MAAM,oCAAoC;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,EAAE;;AAGT,MAAa,WAKT;CACF,gBAAgB;CAChB,iBAAiB;CACjB,cAAc;CACd,eAAe;AACjB;AA8BA,MAAM,aAAa;AACnB,MAAM,gBAAgB;;;;;;AAOtB,SAAS,QAAQ,MAAc,QAAyB;CACtD,MAAM,aAAa,KAAK,SAAS,KAAK,IAAI,OAAO,KAAK;CACtD,MAAM,SAAS,IAAI,IAAI,YAAY,oBAAoB;CACvD,MAAM,gBAAgB,WAAW,YAAY,CAAC,CAAC,WAAW,UAAU;CACpE,MAAM,SAAS,WAAW,MAAM,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,YAAY,CAAC,CAAC,CAAC,MAAM;CACxE,MAAM,WAAW,OAAO,MAAM,OAAO,YAAY,GAAG,IAAI,CAAC;CACzD,MAAM,WAAW,SAAS,WAAW,GAAG,IACpC,SAAS,MAAM,SAAS,QAAQ,GAAG,IAAI,CAAC,IACxC;CAEJ,MAAM,OADe,cAAc,KAAK,QAAQ,CAAC,GAAG,MAElC,OAAO,gBAAgB,aAAa,YAAY;CAClE,MAAM,SAAS,iBAAiB,SAAS,UAAU;CACnD,MAAM,WACJ,OAAO,YAAY,OAAO,WACtB,GAAG,OAAO,SAAS,GAAG,OAAO,SAAS,KACtC;CACN,MAAM,OAAO,OAAO,aAAa,MAAM,KAAK,OAAO;CACnD,OAAO,GAAG,OAAO,KAAK,WAAW,OAAO,SAAS,GAAG,OAAO;AAC7D;;;;;AAMA,SAAS,iBAAiB,SAAgC;CACxD,IAAI,YAAY,MACd,OAAO;CAET,IAAI,EAAE,UAAU,IACd,MAAM,IAAI,WACR,uCAAuC,QAAQ,oEACjD;CAEF,OAAO,UAAU;AACnB;AAEA,eAAe,aACb,WACA,YACiC;CACjC,IAAI,CAAC,WACH,MAAM,IAAI,gBACR,2DACF;CAGF,MAAM,CAAC,EAAE,yBAAyB,EAAE,oBAAoB,MAAM,QAAQ,IAAI,CACxE,OAAO,kCACP,OAAO,qCACT,CAAC;CACD,MAAM,iBAAiB,sBAAsB;CAC7C,IAAI;EAGF,MAAM,eAAe;CACvB,QAAQ;EACN,MAAM,IAAI,gBACR,oEACF;CACF;CAGA,OAAO,eAAe;EACpB,QAAQ;EACR,SAAS;EACT;CACF,CAAC;AACH;;;;;;;AAQA,eAAsB,aACpB,OACA,UAA2B,CAAC,GACX;CACjB,MAAM,EACJ,SAAS,OACT,cAAc,MACd,8BAA8B,OAC9B,WAAW,MACX,WAAW,MACX,UAAU,yBACV,WAAW,SACX,YAAY,MACZ,aAAa,SACX;CACJ,MAAM,WAAW,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;CACtD,OAAO,MACL,mCAAmC,OAAO,qBAAqB,QAAQ,CAAC,EAAE,YAAY,OAAO,MAAM,GACrG;CACA,MAAM,gBAA+B;EACnC,OAAO,SAAS,KAAI,SAAQ,QAAQ,MAAM,QAAQ,MAAM,CAAC,CAAC;EAM1D,0BAA0B;EAC1B,qCAAqC;CACvC;CACA,IAAI,QACF,cAAc,MAAM;EAClB,oBAAoB,CAAC;EACrB,GAAI,cAAc,EAAE,IAAI,MAAM,SAAS,WAAW,EAAE,IAAI,CAAC;CAC3D;CAEF,MAAM,sBAAsB,YAAY,QAAA,CAAS,KAAK,CAAC,CAAC,YAAY;CACpE,IAAI,uBAAuB,YACzB,OAAO,OAAO,eAAe,MAAM,aAAa,WAAW,UAAU,CAAC;MACjE,IAAI,uBAAuB,SAC5B;MAAA,YAAY,UACd,cAAc,OAAO;GAAE;GAAU;EAAS;CAAA,OAG5C,MAAM,IAAI,gBACR,qCAAqC,SAAS,mCAEhD;CAGF,cAAc,iBAAiB,iBAAiB,OAAO;CACvD,cAAc,YAAY,kBACxB,cAAc,aAAa,SAC7B;CACA,OAAO,IAAI,OAAO,aAAa;AACjC;;;;;;;;;;;;;;AAeA,eAAsB,SACpB,OACA,UAA2B,CAAC,GACX;CACjB,oBAAoB,MAAM,aAAa,OAAO,OAAO;CACrD,OAAO;AACT;;;;;;AAOA,SAAgB,uBAAsC;CACpD,OAAO;AACT;;AAGA,SAAgB,qBAAqB,QAA6B;CAChE,oBAAoB;AACtB;;;;;AAiBA,SAAS,cAAc,QAAgC;CACrD,IAAI,QACF,OAAO;CAET,IAAI,sBAAsB,MACxB,MAAM,IAAI,SAAS,8CAA8C;CAEnE,OAAO;AACT;AAEA,eAAe,YAAY,QAAgB,MAAgC;CACzE,QAAQ,MAAM,OAAO,QAAQ,OAAO,EAAE,OAAO,KAAK,CAAC,EAAA,CAAG;AACxD;;AAGA,eAAe,YACb,QACA,MACA,WAAoC,CAAC,GACrC,WAAgC,MACjB;CACf,MAAM,OAAO,QAAQ,OAAO;EAC1B,OAAO;EACP,MAAM;GACJ,GAAI,OAAO,KAAK,QAAQ,CAAC,CAAC,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;GACvD,GAAI,WAAW,EAAY,SAAgC,IAAI,CAAC;EAClE;CACF,CAAC;AACH;;;;;AAMA,SAAS,sBAAsB,OAAyB;CACtD,IAAI,EAAE,iBAAiB,OAAO,gBAC5B,OAAO;CAGT,OADa,MAAM,KAAK,MACX,OAAO,SAAS;AAC/B;;;;;;;;AASA,eAAsB,cACpB,OACA,WAA2C,MAC3C,WAAgC,MAChC,UAA6B,CAAC,GACf;CACf,KAAK,MAAM,QAAQ,OACjB,IAAI;EAIF,MAAM,SAAS,cAAc,QAAQ,MAAM;EAC3C,IAAI,CAAE,MAAM,YAAY,QAAQ,IAAI,GAAI;GACtC,OAAO,MAAM,8BAA8B,MAAM;GACjD,IAAI;IACF,MAAM,YACJ,QACA,MACA,YAAY;KAAE,kBAAkB;KAAG,oBAAoB;IAAE,GACzD,QACF;GACF,SAAS,OAAO;IAId,IAAI,CAAC,sBAAsB,KAAK,GAC9B,MAAM;GAEV;EACF;CACF,SAAS,OAAO;EACd,MAAM,IAAI,gBAAgB,qBAAqB,aAAa,KAAK,GAAG;CACtE;AAEJ;AAEA,SAAS,gBAAgB,QAAiC;CACxD,IAAI,QAAQ,iBAAiB,IAAI,MAAM;CACvC,IAAI,UAAU,KAAA,GAAW;EACvB,QAAQ;GAAE,uBAAO,IAAI,IAAI;GAAG,yBAAS,IAAI,IAAI;EAAE;EAC/C,iBAAiB,IAAI,QAAQ,KAAK;CACpC;CACA,OAAO;AACT;;;;;;;AAQA,SAAS,YAAY,OAAyB;CAC5C,IAAI,EAAE,iBAAiB,OAAO,kBAAkB,MAAM,eAAe,KACnE,OAAO;CAGT,OADa,MAAM,KAAK,MACX,OAAO,SAAS;AAC/B;;AAGA,SAAS,WAAW,OAAyB;CAC3C,OAAO,iBAAiB,OAAO,iBAAiB,MAAM,eAAe;AACvE;;;;;;;;AASA,eAAe,iBACb,QACA,MACkB;CAClB,IAAI;EAIF,SAFiB,MADM,OAAO,QAAQ,cAAc,EAAE,KAAK,CAAC,EAAA,CAClC,KACvB,gBACgB,CAAC,EAAA,CAAG,MAAK,WAAU,OAAO,SAAS,IAAI;CAC5D,SAAS,OAAO;EACd,IAAI,WAAW,KAAK,KAAK,YAAY,KAAK,GACxC,OAAO;EAET,MAAM;CACR;AACF;AAEA,eAAe,gBACb,QACA,MACA,MACe;CACf,IAAI,YAAY;CAChB,IAAI;EACF,IAAI,eAAe,MAAM,iBAAiB,QAAQ,IAAI;EAGtD,IAAI,CAAC,gBAAiB,MAAM,YAAY,QAAQ,IAAI,GAAI;GAEtD,eAAe,MAAM,iBAAiB,QAAQ,IAAI;GAClD,YAAY,CAAC;EACf;EACA,IAAI,CAAC,WAAW;GACd,MAAM,OAAO,QAAQ,iBAAiB;IAAE;IAAM;GAAK,CAAC;GACpD,IAAI,CAAC,cAAc;IACjB,OAAO,MAAM,oCAAoC,MAAM;IACvD,IAAI;KACF,MAAM,OAAO,QAAQ,iBAAiB,EAAE,KAAK,CAAC;IAChD,SAAS,OAAO;KAEd,IAAI,CAAC,sBAAsB,KAAK,GAC9B,MAAM;IAEV;GACF;EACF;CACF,SAAS,OAAO;EACd,MAAM,IAAI,gBAAgB,qBAAqB,aAAa,KAAK,GAAG;CACtE;CACA,IAAI,WACF,MAAM,IAAI,gBACR,mDAAmD,KAAK,kDAE1D;AAEJ;;;;;;;;;;;;;;;;;;;;;;;AAwBA,eAAsB,kBACpB,OACA,WAA2C,MAC3C,WAAgC,MAChC,UAA6B,CAAC,GACf;CACf,MAAM,oBAAoB,WACtB,EAAE,GAAG,SAAS,IACd;EAAE,kBAAkB;EAAG,oBAAoB;CAAE;CAEjD,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI;EACJ,IAAI;GACF,SAAS,cAAc,QAAQ,MAAM;EACvC,SAAS,OAAO;GACd,MAAM,IAAI,gBAAgB,qBAAqB,aAAa,KAAK,GAAG;EACtE;EACA,MAAM,OAAO;GACX,gBAAgB,CAAC,IAAI;GACrB,aAAa,CAAC;GACd,UAAU;GACV,UAAU;IACR,UAAU;IACV,UAAU,kBAAkB,QAAQ;GACtC;GACA,OAAO,EACL,aACE,qFAEJ;EACF;EACA,MAAM,MAAM,KAAK,UAAU;GAAE;GAAM;EAAK,CAAC;EACzC,MAAM,QAAQ,gBAAgB,MAAM;EACpC,IAAI,MAAM,MAAM,IAAI,IAAI,MAAM,KAC5B;EAEF,IAAI,UAAU,MAAM,QAAQ,IAAI,GAAG;EACnC,IAAI,YAAY,KAAA,GAAW;GACzB,UAAU,gBAAgB,QAAQ,MAAM,IAAI,CAAC,CAAC,cAAc;IAC1D,MAAM,QAAQ,OAAO,GAAG;GAC1B,CAAC;GACD,MAAM,QAAQ,IAAI,KAAK,OAAO;EAChC;EACA,MAAM;EACN,MAAM,MAAM,IAAI,MAAM,GAAG;CAC3B;AACF;AAEA,MAAM,kBAAkB;AAGxB,MAAM,CAAC,mBAAmB,IAAI,iBAAiB,MAAM,gBAAgB,MAAM,GAAG;AAE9E,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;;;AAOA,eAAe,kBACb,QACA,OACwB;CACxB,MAAM,YACJ,MAAM,OAAO,QAAQ,gBAAgB;EAAE;EAAO,QAAQ,CAAC,eAAe;CAAE,CAAC,EAAA,CACzE;CACF,MAAM,CAAC,aAAa,MAAM,OAAO,KAAK,QAAQ;CAC9C,IAAI,WAAoC,SAAS,WAAW,EAAE,YAAY,CAAC;CAC3E,IAAI,EAAE,mBAAmB,WACvB,KAAK,MAAM,YAAY,CAAC,OAAO,MAAM,GAAG;EACtC,MAAM,QAAQ,SAAS;EACvB,IAAI,SAAS,KAAK,GAAG;GACnB,WAAW;GACX;EACF;CACF;CAEF,MAAM,eAAe,SAAS;CAC9B,IAAI,CAAC,SAAS,YAAY,KAAK,OAAO,KAAK,YAAY,CAAC,CAAC,WAAW,GAClE,OAAO;CAET,MAAM,UAAU,SAAS,aAAa,OAAO,IAAI,aAAa,UAAU,CAAC;CACzE,MAAM,OAAO,SAAS,QAAQ,eAAe,IAAI,QAAQ,kBAAkB,CAAC;CAC5E,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AACrD;AAgBA,eAAe,qBACb,QACA,iBACA,SACe;CACf,IAAI,CAAE,MAAM,YAAY,QAAQ,eAAe,GAC7C;CAEF,IAAK,MAAM,kBAAkB,QAAQ,eAAe,MAAO,QACzD;CAEF,MAAM,eAAe,GAAG,gBAAgB,IAAI;CAG5C,MAAM,OAAO,EACX,YAAY,GACT,mBAAmB,EAClB,YAAY,GACT,iBAAiB;EAChB,MAAM;EACN,QAAQ,EAAE,SAAS;GAAE,MAAM;GAAW,cAAc;EAAI,EAAE;CAC5D,EACF,EACF,EACF,EACF;CACA,OAAO,KACL,aAAa,gBAAgB,MAAM,aAAa,IAC3C,gBAAgB,oEAEvB;CAIA,IAAI,MAAM,YAAY,QAAQ,YAAY,GAAG;EAC3C,OAAO,QACL,cAAc,aAAa,sDACG,iBAChC;EACA,MAAM,OAAO,QAAQ,OAAO,EAAE,OAAO,aAAa,CAAC;CACrD;CACA,MAAM,YAAY,QAAQ,YAAY;CACtC,MAAM,OAAO,QAAQ,WAAW;EAC9B,OAAO;EACD;CACR,CAAC;CACD,MAAM,QAAQ,QAAQ,iBAAiB,YAAY;CACnD,MAAM,WAAW,QAAQ,iBAAiB,YAAY;CACtD,MAAM,OAAO,QAAQ,OAAO,EAAE,OAAO,gBAAgB,CAAC;AACxD;;AAGA,eAAe,eAAe,QAAgB,OAAgC;CAC5E,MAAM,EAAE,MAAM,aAAa,MAAM,OAAO,MAAM,EAAE,MAAM,CAAC;CACvD,KAAK,SAAS,SAAS,UAAU,KAAK,GACpC,MAAM,IAAI,gBAAgB,oCAAoC,OAAO;CAEvE,OAAO,SAAS;AAClB;;AAGA,eAAe,WACb,QACA,aACA,aACe;CACf,MAAM,OAAO,QAAQ,QAAQ,EAAE,OAAO,YAAY,CAAC;CACnD,MAAM,cAAc,MAAM,eAAe,QAAQ,WAAW;CAC5D,MAAM,cAAc,MAAM,eAAe,QAAQ,WAAW;CAC5D,IAAI,gBAAgB,aAClB,MAAM,IAAI,gBACR,GAAG,YAAY,SAAS,YAAY,cAC/B,YAAY,SAAS,aAC5B;AAEJ;AASA,eAAe,gBACb,QACA,QACe;CACf,KAAK,MAAM,QAAQ,OAAO,OAAO;EAC/B,MAAM,UAAU,GAAG,KAAK;EACxB,IAAI;GAOF,MAAM,EAAE,WAAU,MANU,OAAO,MAAM;IACvC,OAAO;IACP,MAAM,EAAE,OAAO,OAAO,MAAM;IAC5B,oBAAoB;IACpB,kBAAkB;GACpB,CAAC,EAAA,CAC+B;GAChC,IAAI,CAAC,OACH;GAaF,MAAM,UACH,MAZ0B,OAAO,cAAc;IAChD,OAAO;IACP,MAAM;KACJ,OAAO,OAAO;KACd,QAAQ;MAAE,QAAQ,OAAO;MAAQ,MAAM;KAAW;IACpD;IACA,WAAW;IACX,qBAAqB;IACrB,oBAAoB;IACpB,kBAAkB;GACpB,CAAC,EAAA,CAEiB,MAAwC,QAAQ;GAClE,OAAO,KACL,eAAe,OAAO,OAAO,MACxB,MAAM,yBAAyB,QAAQ,SAAS,MAAM,MAAM,EAAE,EACrE;EACF,SAAS,OAAO;GACd,OAAO,QACL,8BAA8B,OAAO,OAAO,gBACvC,QAAQ,IAAI,aAAa,KAAK,EAAE,wIAIvC;EACF;CACF;AACF;;;;;;;;;;;;;;;;AAiBA,eAAsB,eACpB,UAAiC,CAAC,GACnB;CACf,MAAM,EAAE,kBAAkB,gBAAgB,oBAAoB;CAC9D,MAAM,eAAe,QAAQ,kBAAkB,MAAM;CACrD,MAAM,aAAa,QAAQ,gBAAgB,MAAM;CACjD,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,iBAAiB,QACpD;CAGF,MAAM,UAAU;CAChB,KAAK,MAAM,mBAAmB,mBAAmB,CAAC,GAChD,IAAI;EACF,MAAM,qBACJ,cAAc,QAAQ,MAAM,GAC5B,iBACA,OACF;CACF,SAAS,OAAO;EACd,OAAO,QACL,uDACK,gBAAgB,IAAI,aAAa,KAAK,EAAE,4CAE/C;CACF;CAGF,IAAI,CAAC,gBAAgB,CAAC,YACpB;CAGF,IAAI;CACJ,IAAI;EACF,SAAS,cAAc,QAAQ,MAAM;CACvC,SAAS,OAAO;EACd,OAAO,QACL,wJAEuC,aAAa,KAAK,EAAE,4CAE7D;EACA;CACF;CACA,MAAM,gBAAgB,QAAQ;EAC5B,OAAO,oBAAoB,CAAC;EAC5B,OAAO;EACP,QAAQ;EACR,QAAQ;CACV,CAAC;CACD,MAAM,gBAAgB,QAAQ;EAC5B,OAAO,kBAAkB,CAAC;EAC1B,OAAO;EACP,QAAQ;EACR,QAAQ;CACV,CAAC;AACH;;AA+BA,eAAe,cACb,SACA,OACA,OACiB;CACjB,QAAQ,MAAM,eAAe,SAAS,OAAO,KAAK,EAAA,CAAG;AACvD;;AAGA,eAAe,eACb,SACA,OACA,OACoB;CAOpB,QAAQ,MANe,cAAc,QAAQ,MAAM,CAAC,CAAC,OAAO;EAC1D;EAEA,oBAAoB;EACpB,MAAM,EAAE,MAAM;CAChB,CAAC,EAAA,CACgB,KAAK,KAAK,KAAiC,KAC1D,QAAO,IAAI,OACb;AACF;;AAGA,eAAe,cACb,SACA,OACA,OACA,MACoB;CAMpB,QAAQ,MALe,cAAc,QAAQ,MAAM,CAAC,CAAC,OAAO;EAC1D;EACA,oBAAoB;EACpB,MAAM;GAAE;GAAO;GAAM,SAAS,CAAC,GAAG,uBAAuB;EAAE;CAC7D,CAAC,EAAA,CACgB,KAAK,KAAK,KAAiC,KAC1D,QAAO,IAAI,OACb;AACF;;AAGA,eAAe,aACb,SACA,QACA,OACA,KACe;CAGf,MAAM,OAAO,gBAAgB,kBAAkB,QAAQ,GAAG,CAAC;CAM3D,MAAM,cAAc,QAAQ,MAAM,CAAC,CAAC,MAAM;EACxC;EACA;EACA,SAAS;CACX,CAAC;AACH;;AAGA,SAAS,iBAAiB,MAAc,SAA8B;CACpE,IAAI,QAAQ;CACZ,IAAI,QAAQ,aACV,QAAQ,GAAG,MAAM,GAAG,QAAQ;CAE9B,IAAI,QAAQ,aACV,QAAQ,GAAG,QAAQ,cAAc;CAEnC,OAAO;AACT;;;;;;;;AASA,SAAS,cAAc,MAAc,SAA8B;CACjE,MAAM,OAAO,iBAAiB,MAAM,OAAO;CAC3C,OAAO,GAAG,KAAK,KAAK;AACtB;AAEA,SAAS,cAAc,SAA+C;CACpE,OAAO;EACL,kBAAkB,QAAQ,kBAAkB;EAC5C,oBAAoB,QAAQ,oBAAoB;CAClD;AACF;AAEA,SAAS,UAAU,MAAkB,SAAqC;CACxE,OAAO,KAAK,SAAS,UAAU,UAAU,UAAU;AACrD;AAEA,SAAS,UAAU,QAA4C;CAC7D,OACE,WAAW,QAAQ,WAAW,KAAA,KAAa,OAAO,YAAY,MAAM;AAExE;;AAGA,IAAM,gBAAN,MAAoB;CAClB;CACA,yBAAkB,IAAI,IAAY;CAClC,UAAU;CAEV,YAAY,SAAsB;EAChC,KAAK,SAAS,QAAQ,uBAAuB;CAC/C;;CAGA,KAAK,MAAsB;EACzB,IACE,KAAK,WAAW,KAChB,KAAK,OAAO,IAAI,IAAI,KACpB,KAAK,OAAO,OAAO,KAAK,QACxB;GACA,KAAK,OAAO,IAAI,IAAI;GACpB,OAAO;EACT;EACA,MAAM,CAAC,QAAQ,QAAQ,KAAK;EAC5B,IAAI,CAAC,KAAK,SAAS;GACjB,KAAK,UAAU;GACf,OAAO,QACL,uCAAuC,KAAK,OAAO,4CACd,OACvC;EACF;EACA,OAAO;CACT;AACF;;;;;;;;AAaA,eAAsB,gCACpB,iBACA,UAAuB,CAAC,GACT;CACf,OAAO,KAAK,uCAAuC;CACnD,MAAM,WAAW,gBAAgB;CACjC,MAAM,SAAS,gBAAgB;CAC/B,MAAM,EAAE,SAAS,aAAa;CAC9B,MAAM,SAAS,UAAU,OAAO,MAAM;CACtC,MAAM,YAAY,yBAAyB,SAAS,WAAW,EAC7D,OAAO,KACT,CAAC;CACD,MAAM,UAAU,yBAAyB,SAAS,SAAS,EAAE,OAAO,KAAK,CAAC;CAE1E,MAAM,cAAc,cAAc,mBAAmB,OAAO;CAC5D,MAAM,QAAQ,WAAW;EACvB,EAAE,cAAc,EAAE,UAAU,QAAQ,EAAE;EACtC,EAAE,cAAc,EAAE,WAAW,SAAS,EAAE;EACxC,EAAE,cAAc,EAAE,2BAA2B,OAAO,EAAE;EACtD,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,UAAU,EAAE,EAAE,EAAE;EACxD,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,UAAU,EAAE,EAAE,EAAE;CACtD,CAAC;CACD,MAAM,iBAAiB,UAAU,SAAS,oBAAoB;CAC9D,MAAM,eAAe,QAAQ,SAAS,oBAAoB;CAE1D,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,eAAe,SAAS,aAAa,KAAK;CAC7D,SAAS,OAAO;EACd,MAAM,IAAI,gBACR,kDAAkD,aAAa,KAAK,GACtE;CACF;CACA,MAAM,qBACJ,IAAI,aACF,0BAA0B,SAAS,QAAQ,QAAQ,SAAS,MAAM,MAAM,EAAE,wBAChD,eAAe,UAAU,aAAa,kCAElE;CACF,MAAM,UAAU,gBAAgB;CAChC,MAAM,kBACJ,QACA,mBACiE;EACjE,MAAM,cAAc,yBAAyB,OAAO,eAAe;GACjE,OAAO;GACP,WAAW;EACb,CAAC;EACD,MAAM,YAAY,yBAAyB,OAAO,aAAa;GAC7D,OAAO;GACP,WAAW;EACb,CAAC;EACD,MAAM,EAAE,QAAQ,iBAAiB,gBAAgB;EACjD,MAAM,SAAkC;GACtC,YAAY,gBAAgB;GAC5B,eAAe,gBAAgB,gBAAgB;GAC/C,UAAU,SAAS;GACnB,WAAW,SAAS;GACpB,wBAAwB,SAAS;GACjC,WAAW,SAAS;GACpB,YAAY,CAAC,aAAa,SAAS;GACnC,YAAY;GACZ,UAAU;GACV,qBAAqB,OAAO,sBAAsB;GAClD,QAAQ,SAAS;GACjB,kBAAkB;GAClB,mBAAmB,OAAO;GAC1B,gBAAgB,OAAO;GACvB,oBAAoB,OAAO;GAC3B,oBAAoB,OAAO;GAC3B,aAAa,OAAO;GACpB,aAAa,OAAO;GACpB,YAAY,OAAO;GACnB,gBAAgB,OAAO;GACvB,kBAAkB,OAAO;GACzB,eAAe,OAAO;GACtB,aAAa,gBAAgB;GAC7B,cAAc,UAAU,gBAAgB,IAAI;GAC5C,aAAa,UAAU,gBAAgB,GAAG;GAC1C,aAAa,YAAY;GACzB,eAAe,YAAY;GAC3B,aAAa,YAAY;GACzB,IAAI,OAAO,MAAM;GACjB,SAAS,OAAO,WAAW;GAC3B,kBAAkB,OAAO,mBAAmB;GAC5C,WAAW,SAAS,aAAa;IAChC,kBAAkB,YAAY,UAAU;EAC3C;EAEA,KAAK,MAAM,YAAY,gBAAgB,uBACrC,SAAS,QAAQ,oBAAoB;GACnC,MAAM,SAAS;GACf,SAAS,SAAS;EACpB,CAAC;EAEH,KAAK,MAAM,QAAQ,OAAO,YAAY,MAAM;GAC1C,SAAS,QAAQ,gBAAgB;IAC/B,QAAQ,KAAK;IACb,UAAU,KAAK;IACf,QAAQ,KAAK;IACb,cAAc,KAAK,eAAe;GACpC,CAAC;GACD,SACE,QACA,yBACA,GAAG,KAAK,SAAS,KAAK,KAAK,OAAO,KAAK,KAAK,QAC9C;EACF;EACA,KAAK,MAAM,OAAO,OAAO,YAAY,KAAK;GACxC,SAAS,QAAQ,eAAe;IAC9B,QAAQ,IAAI;IACZ,OAAO,IAAI;IACX,QAAQ,IAAI;IACZ,cAAc,IAAI,eAAe;GACnC,CAAC;GACD,SACE,QACA,wBACA,GAAG,IAAI,MAAM,KAAK,IAAI,OAAO,KAAK,IAAI,QACxC;EACF;EACA,OAAO;GAAE;GAAQ;EAAY;CAC/B;CAKA,IAAI,gCAAqC,IAAI,IAAI;CACjD,IAAI,SAAS,SAAS,GAAG;EAEvB,IAAI,YAA8C;EAClD,IAAI,SAAS,SAAS,QAAQ,QAC5B,IAAI;GACF,YAAY,QAAQ,KAAI,WACtB,kBACE,sBACA,eAAe,QAAQ,IAAI,CAAC,CAAC,MAC/B,CACF;EACF,QAAQ;GAEN,YAAY;EACd;EAEF,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,kBAAkB,WAAW,UAAU,OAAM,SAC1D,cAAc,SAAS,aAAa,OAAO,IAAI,CACjD;EACF,SAAS,OAAO;GACd,MAAM,IAAI,gBACR,kDAAkD,aAAa,KAAK,GACtE;EACF;EACA,IAAI,WAAW,MACb,MAAM,aAAa;EAErB,gBAAgB;EAChB,OAAO,QACL,gCAAgC,eAAe,MAAM,QAAQ,CAAC,EAAE,QAC3D,eAAe,MAAM,OAAO,CAAC,EAAE,IAAI,OAAO,KAAK,MAAM,QAAQ,OAAO,mCAE3E;CACF;CACA,MAAM,kBAAkB;EACtB,QAAQ,OAAO;EACf,OAAO,OAAO;EACd,MAAM,OAAO;EACb,GAAG,OAAO;EACV,IAAI,OAAO;EACX,KAAK,cAAc,OAAO,KAAK,KAAK;EACpC,IAAI,OAAO;EACX,IAAI,OAAO,MAAM;EACjB,SAAS,OAAO,WAAW;EAC3B,kBAAkB,OAAO,mBAAmB;CAC9C;CAEA,MAAM,UAAU,IAAI,cAAc,OAAO;CACzC,KAAK,MAAM,CAAC,UAAU,WAAW,QAAQ,QAAQ,GAAG;EAClD,MAAM,EAAE,QAAQ,gBAAgB,eAAe,QAAQ,eAAe;EACtE,IAAI,QAAQ,iBAAiB,mBAAmB,OAAO;EACvD,IAAI,CAAC,QAAQ,gBAEX,QAAQ,QAAQ,KACd,GAAG,MAAM,GAAG,UAAU,aAAa,QAAQ,cAAc,GAC3D;EAEF,IAAI,cAAc,IAAI,QAAQ,GAC5B;EAEF,IAAI,QAAQ,gBACV,MAAM,kBACJ,CAAC,KAAK,GACN,cAAc,OAAO,GACrB,eAAe,WACf,OACF;OAEA,MAAM,cACJ,CAAC,KAAK,GACN,cAAc,OAAO,GACrB,eAAe,WACf,OACF;EAGF,IAAI;GAEF,OAAO,eAAe,OAAO,eAAe,OAAO;GACnD,MAAM,aAAa,SAAS,sBAAsB,OAAO,MAAM;EACjE,SAAS,OAAO;GACd,MAAM,IAAI,gBAAgB,qBAAqB,aAAa,KAAK,GAAG;EACtE;CACF;AACF;;;;;;;AAQA,SAAS,qBAAqB,OAAyB;CACrD,IAAI,SAAS,KAAK,MAAM,GACtB,OAAO;CAET,MAAM,QAAQ,QAAQ,OAAO,CAAC;CAC9B,IAAI,QAAQ,OAAO,CAAC,MAAM,IACxB,OAAO,QAAQ,OAAO,CAAC;CAEzB,OAAO,GAAG,OAAO,MAAM,KAAK,EAAE;AAChC;AAEA,SAAS,SAAS,OAA+B;CAC/C,OAAO,MAAM,QAAQ,KAAK,KAAK,OAAO,UAAU,WAC5C,MAAM,SACN;AACN;;AAGA,SAAS,QAAQ,OAAgB,OAAwB;CACvD,IAAI,MAAM,QAAQ,KAAK,KAAK,OAAO,UAAU,UAAU;EACrD,IAAI,SAAS,MAAM,QACjB,MAAM,IAAI,MAAM,yBAAyB;EAE3C,OAAO,MAAM;CACf;CACA,MAAM,IAAI,UAAU,IAAI,OAAO,MAAM,8BAA8B;AACrE;;AAGA,SAAS,OAAO,WAAmB,OAAwB;CACzD,IAAI,OAAO,UAAU,UACnB,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,SAAS;CAElC,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,IAAI,MAAM,CAAC,CAAC,KAAK,SAAS;CAEzC,MAAM,IAAI,UAAU,2BAA2B;AACjD;AAEA,SAAS,SAAS,OAAyC;CACzD,OAAO,SAAS,KAAK,IAAI,QAAQ,CAAC;AACpC;;;;;;;;AASA,eAAsB,8BACpB,eACA,UAAuB,CAAC,GACT;CACf,OAAO,KAAK,qCAAqC;CACjD,MAAM,eAAe,OAAO,eAAe,cAAc;CACzD,MAAM,kBAAkB,OAAO,cAAc,MAAM;CACnD,IAAI,aAAgC;CACpC,IAAI,oBAAoB,QAAQ,oBAAoB,KAAA,GAClD,aAAa,yBAAyB,OAAO,eAAe,CAAC;CAE/D,MAAM,kBAAkB,SAAS,OAAO,cAAc,SAAS,CAAC;CAChE,MAAM,UAAmC,CAAC;CAC1C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,eAAe,GAGxD,OAAO,SAAS,KAAK,YAAY,CAAC,CAAC,WAAW,KAAK,GAAG,GAAG,KAAK;CAKhE,MAAM,iBAAiB,OAAO,OAAO,eAAe,gBAAgB,CAAC;CACrE,MAAM,cAAc,yBAAyB,gBAAgB,EAC3D,WAAW,KACb,CAAC;CACD,MAAM,+BAA+B,KAAK,MACxC,YAAY,UAAU,IAAI,aAC5B;CAEA,MAAM,cAAc,CAClB,cAAc,iBAAiB,OAAO,GACtC,cAAc,kBAAkB,OAAO,CACzC,CAAC,CAAC,KAAK,GAAG;CACV,MAAM,UAAmB,CACvB,EAAE,OAAO,EAAE,cAAc,6BAA6B,EAAE,CAC1D;CAEA,IAAI,OAAuC;CAC3C,IAAI,KAAqC;CACzC,IAAI,UAAmB;CACvB,IAAI,OAAO,OAAO,SAAS,MAAM,GAAG;EAElC,QAAQ,OAAO,qBAAqB,QAAQ,IAAI;EAChD,IAAI,QAAQ,SAAS,MAAM;GACzB,OAAO,EAAE,uBAAuB,QAAQ,KAAK;GAC7C,QAAQ,KAAK,EAAE,cAAc,KAAK,CAAC;EACrC;CACF;CACA,IAAI,OAAO,OAAO,SAAS,IAAI,GAAG;EAChC,QAAQ,KAAK,qBAAqB,QAAQ,EAAE;EAC5C,IAAI,QAAQ,OAAO,MAAM;GACvB,KAAK,EAAE,qBAAqB,QAAQ,GAAG;GACvC,QAAQ,KAAK,EAAE,cAAc,GAAG,CAAC;EACnC;CACF;CACA,IAAI,OAAO,OAAO,SAAS,UAAU,GAGnC,QAAQ,cAAc,qBAAqB,QAAQ,WAAW;CAEhE,IAAI,OAAO,OAAO,SAAS,SAAS,GAAG;EACrC,UAAU,QAAQ;EAClB,QAAQ,KAAK,EAAE,cAAc,EAAE,0BAA0B,QAAQ,EAAE,CAAC;CACtE;CAEA,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,cAAc,SAAS,aAAa,WAAW,OAAO,CAAC;CAC1E,SAAS,OAAO;EACd,MAAM,IAAI,gBACR,kDAAkD,aAAa,KAAK,GACtE;CACF;CACA,IAAI,WAAW,GACb,MAAM,IAAI,aACR,uBAAuB,MAAM,EAAE,EAAE,QAAQ,MAAM,IAAI,EAAE,qBAC9B,MAAM,OAAO,EAAE,uBAAuB,eAAe,8BAG9E;CAGF,MAAM,SAAkC;EACtC,KAAK,OAAO,eAAe,QAAQ;EACnC;EACA,cAAc,OAAO,eAAe,mBAAmB;EACvD,MAAM;EACN,SAAS,OAAO,cAAc,SAAS;EACvC,uBAAuB,OAAO,cAAc,qBAAqB;EACjE,MAAM,OAAO,cAAc,MAAM;CACnC;CAOA,KAAK,MAAM,CAAC,KAAK,UAAU;EALzB,CAAC,MAAM,IAAI;EACX,CAAC,WAAW,UAAU;EACtB,CAAC,MAAM,IAAI;EACX,CAAC,OAAO,KAAK;CAEuB,GACpC,KAAK,MAAM,WAAW,OAAO,cAAc,GAAG,GAC5C,SAAS,QAAQ,OAAO;EACtB,cAAc,OAAO,SAAS,aAAa;EAC3C,SAAS,OAAO,SAAS,SAAS;CACpC,CAAC;CAGL,KAAK,MAAM,cAAc,OAAO,cAAc,aAAa,GACzD,SAAS,QAAQ,eAAe;EAC9B,UAAU,OAAO,YAAY,UAAU;EACvC,cAAc,OAAO,YAAY,iBAAiB;EAClD,QAAQ,OAAO,YAAY,QAAQ;CACrC,CAAC;CAGH,IAAI;EACF,MAAM,SAAS,OAAO,eAAe,QAAQ;EAC7C,MAAM,aAAsC;GAC1C,eAAe,OAAO,eAAe,cAAc;GACnD,YAAY,OAAO,eAAe,WAAW;GAC7C,SAAS,OAAO,eAAe,SAAS;GACxC,oBAAoB,OAAO,eAAe,kBAAkB;GAC5D,cAAc;GACd,QAAQ,OAAO,eAAe,gBAAgB;GAC9C,sBAAsB,OAAO,eAAe,oBAAoB;GAChE,wBAAwB,OAAO,eAAe,uBAAuB;GACrE,kBAAkB,OAAO,eAAe,gBAAgB;GACxD,mBAAmB,OAAO,QAAQ,WAAW;GAC7C,gBAAgB,OAAO,QAAQ,SAAS;GACxC,oBAAoB,OAAO,QAAQ,YAAY;GAC/C,oBAAoB,OAAO,QAAQ,YAAY;GAC/C,YAAY,OAAO,QAAQ,KAAK;GAChC,gBAAgB,OAAO,QAAQ,QAAQ;GACvC,kBAAkB,OAAO,QAAQ,UAAU;GAC3C,2BAA2B,OACzB,eACA,0BACF;GACA,cAAc,OAAO,eAAe,aAAa;GACjD,aAAa,OAAO,eAAe,YAAY;GAC/C,kBAAkB,OAAO,eAAe,gBAAgB;GACxD;IACC,kBAAkB,YAAY,UAAU;EAC3C;EAEA,IAAI,QAAQ,iBAAiB,iBAAiB,OAAO;EACrD,IAAI,QAAQ,gBACV,MAAM,kBACJ,CAAC,KAAK,GACN,cAAc,OAAO,GACrB,eAAe,SACf,OACF;OACK;GACL,QAAQ,GAAG,MAAM,GAAG,UAAU,aAAa,QAAQ,cAAc;GACjE,MAAM,cACJ,CAAC,KAAK,GACN,cAAc,OAAO,GACrB,eAAe,SACf,OACF;EACF;EACA,IAAI;GACF,MAAM,aAAa,SAAS,oBAAoB,OAAO,UAAU;EACnE,SAAS,OAAO;GACd,MAAM,IAAI,gBAAgB,qBAAqB,aAAa,KAAK,GAAG;EACtE;CACF,SAAS,OAAO;EACd,IAAI,iBAAiB,UACnB,MAAM,IAAI,qBACR,0CAA0C,MAAM,SAClD;EAEF,MAAM;CACR;AACF;;;;;;;AAQA,eAAsB,8BACpB,QACA,UAAuB,CAAC,GACT;CACf,OAAO,KAAK,sCAAsC;CAClD,MAAM,UAAU,OAAO;CACvB,MAAM,EAAE,aAAa;CACrB,MAAM,YAAY,yBAAyB,OAAO,WAAW,EAAE,OAAO,KAAK,CAAC;CAC5E,MAAM,UAAU,yBAAyB,OAAO,SAAS,EAAE,OAAO,KAAK,CAAC;CACxE,MAAM,iBAAiB,UAAU,SAAS,oBAAoB;CAC9D,MAAM,eAAe,QAAQ,SAAS,oBAAoB;CAE1D,MAAM,QAAQ,WAAW;EACvB,EAAE,cAAc,EAAE,UAAU,QAAQ,EAAE;EACtC,EAAE,cAAc,EAAE,WAAW,SAAS,EAAE;EACxC,EAAE,OAAO,EAAE,YAAY,UAAU,UAAU,EAAE,EAAE;EAC/C,EAAE,OAAO,EAAE,UAAU,QAAQ,UAAU,EAAE,EAAE;CAC7C,CAAC;CACD,MAAM,cAAc,cAAc,YAAY,OAAO;CAErD,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,cAAc,SAAS,aAAa,KAAK;CAC5D,SAAS,OAAO;EACd,MAAM,IAAI,gBACR,kDAAkD,aAAa,KAAK,GACtE;CACF;CACA,IAAI,WAAW,GACb,MAAM,IAAI,aACR,yBAAyB,SAAS,SAC5B,QAAQ,wBACT,eAAe,UACf,aAAa,kCAEpB;CAGF,IAAI,QAAQ,iBAAiB,YAAY,OAAO;CAChD,IAAI,CAAC,QAAQ,gBACX,QAAQ,GAAG,MAAM,GAAG,UAAU,WAAW,QAAQ,cAAc;CAIjE,MAAM,aAAsC;EAC1C,UAAU,OAAO;EACjB,YAAY,CAAC,WAAW,OAAO;EAC/B,YAAY;EACZ,UAAU;EACV,cAAc,OAAO;EACrB,WAAW,OAAO;GACjB,kBAAkB,UAAU,UAAU;CACzC;CAEA,KAAK,MAAM,UAAU,OAAO,UAAU;EACpC,MAAM,gBACJ,mBAAmB,SAAS,OAAO,gBAAgB;EACrD,MAAM,iBACJ,oBAAoB,SAAS,OAAO,iBAAiB;EAIvD,MAAM,uBAAuB,OAAO,gBAAgB;EACpD,MAAM,qBAAqB,OAAO,cAAc;EAChD,SACE,YACA,qBACA,GAAG,qBAAqB,KAAK,oBAC/B;EAEA,MAAM,YAAqC;GACzC,eAAe,OAAO;GACtB,aAAa,OAAO;GACpB,0BAA0B,OAAO;GACjC,sBAAsB,OAAO;GAC7B,eAAe,iBAAiB;GAChC,kBAAkB,kBAAkB;EACtC;EACA,IAAI,oBAAoB,QACtB,KAAK,MAAM,UAAU,OAAO,kBAAkB,CAAC,GAAG;GAGhD,MAAM,WAAW;GACjB,IAAI,2BAAoC;GACxC,IAAI,uBAAuB,QACzB,2BAA2B,OAAO;QAC7B,IAAI,8BAA8B,QACvC,2BAA2B,SAAS;GAEtC,MAAM,QAAQ,QACZ,OAAO,SAAS,OAAO,OAAO;GAChC,MAAM,eAAe,KAAK,cAAc;GACxC,MAAM,cAAc,KAAK,aAAa;GACtC,MAAM,sBAAsB,KAAK,qBAAqB;GACtD,SAAS,WAAW,mBAAmB;IACrC,aAAa,OAAO;IACpB,YAAY,KAAK,WAAW;IAC5B,gBAAgB,SAAS,cAAc,gBAAgB;IACvD,uBAAuB;IACvB,mBAAmB,KAAK,iBAAiB;IACzC,cAAc,SAAS,aAAa,cAAc;IAClD,sBAAsB,OAAO;IAC7B,4BAA4B;IAC5B,qBAAqB,KAAK,mBAAmB;GAC/C,CAAC;GACD,SACE,YACA,4BACA;IACE;IACA;IACA,OAAO,cAAc;IACrB,MAAM,gBAAgB,MAAM;IAC5B,MAAM,eAAe,MAAM;IAC3B,MAAM,uBAAuB,MAAM;GACrC,CAAC,CAAC,KAAK,KAAK,CACd;EACF;EAEF,SAAS,YAAY,YAAY,SAAS;CAC5C;CAEA,IAAI,QAAQ,gBACV,MAAM,kBACJ,CAAC,KAAK,GACN,cAAc,OAAO,GACrB,eAAe,SACf,OACF;MAEA,MAAM,cACJ,CAAC,KAAK,GACN,cAAc,OAAO,GACrB,eAAe,SACf,OACF;CAGF,IAAI;EACF,MAAM,aAAa,SAAS,qBAAqB,OAAO,UAAU;CACpE,SAAS,OAAO;EACd,MAAM,IAAI,gBAAgB,qBAAqB,aAAa,KAAK,GAAG;CACtE;AACF;;AAGA,MAAa,iCACX"}
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@tsdmarc/opensearch",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "OpenSearch output for tsdmarc: save DMARC aggregate, failure and SMTP TLS reports to OpenSearch, compatible with parsedmarc's OpenSearch Dashboards.",
5
+ "license": "Apache-2.0",
6
+ "author": "HelixNebula (https://helixnebula.tech)",
7
+ "homepage": "https://github.com/helixnebulatech/tsdmarc/tree/main/packages/opensearch#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/helixnebulatech/tsdmarc/issues"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ "./package.json": "./package.json",
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist/**/*",
25
+ "CHANGELOG.md",
26
+ "README.md",
27
+ "LICENSE",
28
+ "NOTICE"
29
+ ],
30
+ "dependencies": {
31
+ "@aws-sdk/credential-providers": "^3.1157.0",
32
+ "@opensearch-project/opensearch": "^3.9.0"
33
+ },
34
+ "devDependencies": {
35
+ "@arethetypeswrong/cli": "0.18.5",
36
+ "@tsdmarc/search-common": "0.0.0",
37
+ "@tsdmarc/tsconfig": "0.0.0",
38
+ "@tsdmarc/tsdown-config": "0.0.0",
39
+ "@types/node": "26.6.5",
40
+ "tsdmarc": "1.0.0",
41
+ "tsdown": "0.23.2",
42
+ "vitest": "5.0.5"
43
+ },
44
+ "peerDependencies": {
45
+ "tsdmarc": "^1.0.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=24"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "provenance": true
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/helixnebulatech/tsdmarc.git",
57
+ "directory": "packages/opensearch"
58
+ },
59
+ "keywords": [
60
+ "dmarc",
61
+ "tsdmarc",
62
+ "opensearch",
63
+ "opensearch-dashboards",
64
+ "aws",
65
+ "dmarc-reports",
66
+ "smtp-tls",
67
+ "parsedmarc"
68
+ ],
69
+ "scripts": {
70
+ "build": "tsdown",
71
+ "clean": "rm -rf dist *.tsbuildinfo",
72
+ "publint": "publint && attw --pack . --profile esm-only --ignore-rules cjs-resolves-to-esm",
73
+ "test": "vitest --run",
74
+ "test:watch": "vitest",
75
+ "type-check": "tsrs --noEmit -p tsconfig.json"
76
+ }
6
77
  }