(function (root, factory) { "use strict"; const comparison = typeof module === "object" && module.exports ? require("./comparison-workflow.js") : root?.MbnComparisonWorkflow; const api = Object.freeze(factory(comparison)); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnComparisonImportWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function (comparison) { "use strict"; if (!comparison) throw new Error("Comparison workflow dependency is unavailable."); const IMPORT_SCHEMA_VERSION = 1; const STORAGE_ENVELOPE_SCHEMA_VERSION = 1; const MAXIMUM_PAIRS = 500; const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/; const digestPattern = /^[A-F0-9]{64}$/; const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$/; const controlCharacterPattern = /[\u0000-\u001f\u007f]/u; const importedRecordIdPattern = /^legacy-compare-[a-f0-9]{12}-[1-9][0-9]{0,2}$/u; function hasOnlyKeys(value, expected) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const keys = Object.keys(value).sort(); const wanted = [...expected].sort(); return keys.length === wanted.length && keys.every((key, index) => key === wanted[index]); } function safeText(value, maximumLength) { return typeof value === "string" && value.length > 0 && value.length <= maximumLength && value === value.trim() && !controlCharacterPattern.test(value) ? value : null; } function normalizeWireTarget(value) { if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.kind !== "string") return null; let candidate; switch (value.kind) { case "market-target": if (!hasOnlyKeys(value, ["kind", "target"]) || !safeText(value.target, 64)) return null; candidate = { kind: value.kind, target: value.target }; break; case "krx-stock": case "nxt-stock": if (!hasOnlyKeys(value, ["kind", "market", "stockName", "stockCode"]) || !safeText(value.market, 8) || !safeText(value.stockName, 200) || !safeText(value.stockCode, 32)) return null; candidate = { kind: value.kind, market: value.market, stockName: value.stockName, stockCode: value.stockCode }; break; case "world-stock": if (!hasOnlyKeys(value, ["kind", "inputName", "symbol", "nation"]) || !safeText(value.inputName, 200) || !safeText(value.symbol, 64) || !safeText(value.nation, 2)) return null; candidate = { kind: value.kind, inputName: value.inputName, displayName: value.inputName, symbol: value.symbol, nation: value.nation }; break; default: return null; } return comparison.normalizeTarget(candidate); } function normalizeImportResponse(value, expectedRequestId) { if (!requestIdPattern.test(String(expectedRequestId || "")) || !hasOnlyKeys(value, ["requestId", "sourceSha256", "sourceRowCount", "retrievedAt", "pairs"]) || value.requestId !== expectedRequestId || typeof value.sourceSha256 !== "string" || !digestPattern.test(value.sourceSha256) || !Number.isInteger(value.sourceRowCount) || value.sourceRowCount < 1 || value.sourceRowCount > MAXIMUM_PAIRS || typeof value.retrievedAt !== "string" || !isoDatePattern.test(value.retrievedAt) || !Number.isFinite(Date.parse(value.retrievedAt)) || !Array.isArray(value.pairs) || value.pairs.length !== value.sourceRowCount) return null; const pairs = []; const signatures = new Set(); for (let index = 0; index < value.pairs.length; index += 1) { const raw = value.pairs[index]; if (!hasOnlyKeys(raw, ["rowNumber", "first", "second"]) || raw.rowNumber !== index + 1) return null; const first = normalizeWireTarget(raw.first); const second = normalizeWireTarget(raw.second); const pair = comparison.normalizePair([first, second]); if (!pair) return null; const signature = pair.map(comparison.targetKey).join("\u001f"); if (!signature || signatures.has(signature)) return null; signatures.add(signature); pairs.push(Object.freeze({ rowNumber: index + 1, pair })); } return Object.freeze({ requestId: value.requestId, sourceSha256: value.sourceSha256, sourceRowCount: value.sourceRowCount, retrievedAt: value.retrievedAt, pairs: Object.freeze(pairs) }); } function pairSignature(value) { const pair = comparison.normalizePair(value); return pair ? pair.map(comparison.targetKey).join("\u001f") : null; } function mergeImport(currentValue, importValue) { const current = comparison.normalizePairList(currentValue); if (!importValue || !Array.isArray(importValue.pairs) || !digestPattern.test(String(importValue.sourceSha256 || "")) || !Number.isInteger(importValue.sourceRowCount) || importValue.sourceRowCount !== importValue.pairs.length || importValue.sourceRowCount < 1 || importValue.sourceRowCount > MAXIMUM_PAIRS || current.pairs.length + importValue.pairs.length > MAXIMUM_PAIRS) { throw new RangeError("The legacy comparison import cannot fit the saved-pair schema."); } const ids = new Set(current.pairs.map(record => record.id)); const signatures = new Set(current.pairs.map(record => pairSignature(record.pair))); let merged = current; for (let index = 0; index < importValue.pairs.length; index += 1) { const imported = importValue.pairs[index]; if (imported?.rowNumber !== index + 1) { throw new TypeError("The legacy comparison import row order is invalid."); } const signature = pairSignature(imported?.pair); if (!signature || signatures.has(signature)) { throw new RangeError("The legacy comparison import contains an existing pair."); } const id = `legacy-compare-${importValue.sourceSha256.slice(0, 12).toLocaleLowerCase("en-US")}-${imported.rowNumber}`; if (ids.has(id)) { throw new RangeError("The legacy comparison import id already exists."); } const record = comparison.createPairRecord(id, imported.pair); if (!record) throw new TypeError("The legacy comparison import pair is invalid."); merged = comparison.appendPair(merged, record); ids.add(id); signatures.add(signature); } return merged; } function createMarker(importValue, importedAt = new Date().toISOString()) { if (!importValue || !digestPattern.test(String(importValue.sourceSha256 || "")) || !Number.isInteger(importValue.sourceRowCount) || importValue.sourceRowCount < 1 || importValue.sourceRowCount > MAXIMUM_PAIRS || typeof importedAt !== "string" || !isoDatePattern.test(importedAt) || !Number.isFinite(Date.parse(importedAt))) return null; return Object.freeze({ schemaVersion: IMPORT_SCHEMA_VERSION, sourceSha256: importValue.sourceSha256, importedPairCount: importValue.sourceRowCount, importedAt }); } function normalizeMarker(value) { if (!hasOnlyKeys(value, ["schemaVersion", "sourceSha256", "importedPairCount", "importedAt"]) || value.schemaVersion !== IMPORT_SCHEMA_VERSION || !digestPattern.test(String(value.sourceSha256 || "")) || !Number.isInteger(value.importedPairCount) || value.importedPairCount < 1 || value.importedPairCount > MAXIMUM_PAIRS || typeof value.importedAt !== "string" || !isoDatePattern.test(value.importedAt) || !Number.isFinite(Date.parse(value.importedAt))) return null; return Object.freeze({ schemaVersion: IMPORT_SCHEMA_VERSION, sourceSha256: value.sourceSha256, importedPairCount: value.importedPairCount, importedAt: value.importedAt }); } function normalizeExactPairList(value) { if (!hasOnlyKeys(value, ["schemaVersion", "pairs"]) || !Array.isArray(value.pairs) || value.pairs.length > MAXIMUM_PAIRS || value.pairs.some(record => !hasOnlyKeys(record, ["id", "pair"]))) return null; const normalized = comparison.normalizePairList(value); if (normalized.pairs.length !== value.pairs.length || normalized.pairs.some((record, index) => record.id !== value.pairs[index].id)) return null; return normalized; } function normalizeStorageEnvelope(value) { if (!hasOnlyKeys(value, ["schemaVersion", "pairList", "legacyImportMarker"]) || value.schemaVersion !== STORAGE_ENVELOPE_SCHEMA_VERSION) return null; const pairList = normalizeExactPairList(value.pairList); const marker = value.legacyImportMarker === null ? null : normalizeMarker(value.legacyImportMarker); if (!pairList || (value.legacyImportMarker !== null && !marker) || (!marker && pairList.pairs.some(record => importedRecordIdPattern.test(record.id)))) { return null; } return Object.freeze({ schemaVersion: STORAGE_ENVELOPE_SCHEMA_VERSION, pairList, legacyImportMarker: marker }); } function createStorageEnvelope(pairList, marker = null) { return normalizeStorageEnvelope({ schemaVersion: STORAGE_ENVELOPE_SCHEMA_VERSION, pairList, legacyImportMarker: marker }); } return { schemaVersion: IMPORT_SCHEMA_VERSION, storageEnvelopeSchemaVersion: STORAGE_ENVELOPE_SCHEMA_VERSION, maximumPairs: MAXIMUM_PAIRS, normalizeImportResponse, mergeImport, createMarker, normalizeMarker, createStorageEnvelope, normalizeStorageEnvelope }; });