@owox/connectors 0.36.0 → 0.37.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.
Files changed (3) hide show
  1. package/dist/index.cjs +393 -93
  2. package/dist/index.js +393 -93
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -349,6 +349,7 @@ var require_index = __commonJS({
349
349
  async run() {
350
350
  var _a;
351
351
  this.context.emit(new ControlEvent(CONTROL_ACTION.STARTED));
352
+ this._rowsByNode = /* @__PURE__ */ new Map();
352
353
  try {
353
354
  this.context.validate();
354
355
  this._processRunConfig();
@@ -375,6 +376,7 @@ var require_index = __commonJS({
375
376
  }
376
377
  }
377
378
  this._reportAccountOutcomes(state);
379
+ this._reportEmptyNodes([...plainNodes, ...timeSeriesNodes]);
378
380
  this.source.onImportComplete(this.context);
379
381
  this.context.emit(new ControlEvent(CONTROL_ACTION.COMPLETED));
380
382
  } catch (error) {
@@ -510,10 +512,11 @@ var require_index = __commonJS({
510
512
  * @param {object} state run state from _createRunState
511
513
  * @param {object|null} account the account this unit belongs to
512
514
  * @param {Function} work async thunk performing the fetch + write
515
+ * @param {string} subject what the unit imports -- a node name, or the day for day-by-day
513
516
  * @returns {Promise<boolean>} true when the unit completed
514
517
  * @private
515
518
  */
516
- async _runForAccount(state, account, work) {
519
+ async _runForAccount(state, account, work, subject) {
517
520
  try {
518
521
  await work();
519
522
  state.succeeded.add(this._accountKey(account));
@@ -521,7 +524,7 @@ var require_index = __commonJS({
521
524
  } catch (error) {
522
525
  this.source.onAccountError(account, error);
523
526
  if (error == null ? void 0 : error[STORAGE_FAILURE]) throw error;
524
- this._recordAccountFailure(state, account, error);
527
+ this._recordAccountFailure(state, account, error, subject);
525
528
  return false;
526
529
  }
527
530
  }
@@ -560,23 +563,29 @@ var require_index = __commonJS({
560
563
  * @param {object} state run state from _createRunState
561
564
  * @param {object|null} account the account that failed
562
565
  * @param {Error} error the failure to classify
566
+ * @param {string} [subject] what was being imported, named in the report as main did
563
567
  * @private
564
568
  */
565
- _recordAccountFailure(state, account, error) {
569
+ _recordAccountFailure(state, account, error, subject) {
566
570
  const accountId = this._accountKey(account);
567
571
  const isSkip = (error == null ? void 0 : error.isWarning) === true;
568
572
  if (!isSkip) state.passHardFailures += 1;
569
573
  let entry = state.issues.get(accountId);
570
574
  if (!entry) {
571
- entry = { errors: [], reported: /* @__PURE__ */ new Set() };
575
+ entry = { errors: [], reported: /* @__PURE__ */ new Set(), subjects: /* @__PURE__ */ new Set() };
572
576
  state.issues.set(accountId, entry);
573
577
  }
574
578
  entry.errors.push(error);
579
+ if (subject) entry.subjects.add(subject);
575
580
  const level = isSkip ? LOG_LEVEL.WARN : LOG_LEVEL.ERROR;
576
581
  if (entry.reported.has(error.message)) return;
577
582
  entry.reported.add(error.message);
578
583
  const what = account === null || account === void 0 ? isSkip ? "Skipped" : "Import failed" : `${isSkip ? "Skipped account" : "Error processing account"} ${accountId}`;
579
- this.context.log(level, `${what}: ${error.message}`);
584
+ const line = `${what}: ${error.message}`;
585
+ this.context.log(
586
+ level,
587
+ subject ? `Importing ${subject}: ${line[0].toLowerCase()}${line.slice(1)}` : line
588
+ );
580
589
  }
581
590
  /**
582
591
  * Whether the account has failed outright (not merely been skipped) with the same error
@@ -635,7 +644,48 @@ var require_index = __commonJS({
635
644
  */
636
645
  _emitCursor(state, date) {
637
646
  if (state.cursorHalted) return;
638
- this.context.emit(new StateEvent({ lastRequestedDate: date }));
647
+ const claimed = state.cursorCap && date > state.cursorCap ? state.cursorCap : date;
648
+ if (claimed !== date && claimed === state.lastClaimed) return;
649
+ state.lastClaimed = claimed;
650
+ this.context.emit(new StateEvent({ lastRequestedDate: claimed }));
651
+ }
652
+ /**
653
+ * The last day a node is asked for: the run's end, or `endLagDays` before today for an API
654
+ * that reports only completed days, whichever is earlier.
655
+ * @private
656
+ */
657
+ _nodeWindowEnd(node, dateRange) {
658
+ var _a, _b;
659
+ const lag = ((_b = (_a = this.source).getEndLagDays) == null ? void 0 : _b.call(_a, node.name)) ?? 0;
660
+ if (!lag) return dateRange.endDate;
661
+ const today = this._parseDate(this._formatDate(/* @__PURE__ */ new Date()));
662
+ const lagged = this._formatDate(new Date(today - lag * 864e5));
663
+ return lagged < dateRange.endDate ? lagged : dateRange.endDate;
664
+ }
665
+ /**
666
+ * Notes where `endLagDays` stopped a manual backfill short of its EndDate. The run completes
667
+ * either way, and nothing else on it names the days it left out.
668
+ * @private
669
+ */
670
+ _logLagCut(node, dateRange, end) {
671
+ var _a;
672
+ if (((_a = this.context.runConfig) == null ? void 0 : _a.type) !== RUN_CONFIG_TYPE.MANUAL_BACKFILL) return;
673
+ if (end >= dateRange.endDate) return;
674
+ this.context.log(
675
+ LOG_LEVEL.INFO,
676
+ `Node "${node.name}" stops at ${end}, not at the EndDate ${dateRange.endDate}: endLagDays ends its window that many days before today`
677
+ );
678
+ }
679
+ /**
680
+ * Notes a node that has nothing to ask for this run because its lag ends its window
681
+ * before the run's first day.
682
+ * @private
683
+ */
684
+ _logLaggedOut(node, dateRange, end) {
685
+ this.context.log(
686
+ LOG_LEVEL.INFO,
687
+ `Node "${node.name}" asks for nothing this run: endLagDays ends its window on ${end}, before the run's first day ${dateRange.startDate}`
688
+ );
639
689
  }
640
690
  /**
641
691
  * Lists each account's latest error; for a source without accounts, just the error.
@@ -725,7 +775,9 @@ var require_index = __commonJS({
725
775
  } else if (state.accountless) {
726
776
  message = `Nothing was imported because access was refused: ${describe(entries)}`;
727
777
  } else {
728
- message = `All ${state.attemptedCount} accounts were skipped, so nothing was imported. This points to a global failure, such as an expired access token, rather than individual accounts being inaccessible. Errors: ${describe(entries)}`;
778
+ const subjects = new Set(entries.flatMap(([, entry]) => [...entry.subjects]));
779
+ const where = subjects.size === 1 ? ` while importing ${[...subjects][0]}` : "";
780
+ message = `All ${state.attemptedCount} accounts were skipped${where}, so nothing was imported. This points to a global failure, such as an expired access token, rather than individual accounts being inaccessible. Errors: ${describe(entries)}`;
729
781
  }
730
782
  const error = new Error(message);
731
783
  error.isWarning = failed.length === 0;
@@ -808,11 +860,13 @@ var require_index = __commonJS({
808
860
  } catch (error) {
809
861
  throw asStorageFailure(error);
810
862
  }
863
+ this._countRows(writer.nodeName, rows.length);
811
864
  }
812
865
  /**
813
866
  * Resolves short links for the fields a schema node declares under `shortLinks`, writing
814
- * each landing page next to its original. No-op when the node has no spec, Process Short
815
- * Links is off, or no spec has its field (and its `_parsed` target) selected.
867
+ * the address each short link service points to next to the original. No-op when the node
868
+ * has no spec, Process Short Links is off, or no spec has its field (and its `_parsed`
869
+ * target) selected.
816
870
  *
817
871
  * The helpers are bare globals of Core/Utils/ShortLinksUtils.js, a script in the bundle's
818
872
  * scope; nothing here touches them for a node without a spec.
@@ -823,7 +877,7 @@ var require_index = __commonJS({
823
877
  * @returns {Promise<object[]>} records with resolved links, or the same array
824
878
  */
825
879
  async resolveShortLinks(nodeName, data, fields) {
826
- var _a, _b, _c, _d, _e;
880
+ var _a, _b, _c, _d;
827
881
  const specs = (_c = (_b = (_a = this.source) == null ? void 0 : _a.fieldsSchema) == null ? void 0 : _b[nodeName]) == null ? void 0 : _c.shortLinks;
828
882
  if (!Array.isArray(specs) || specs.length === 0 || !data.length) return data;
829
883
  if (((_d = this.context.getParameter("ProcessShortLinks")) == null ? void 0 : _d.value) === false) return data;
@@ -834,12 +888,7 @@ var require_index = __commonJS({
834
888
  if (activeSpecs.length === 0) return data;
835
889
  const cache = this._shortLinksCache();
836
890
  return resolveShortLinkFields(data, activeSpecs, {
837
- nestedPathHosts: [
838
- ...getShortLinkDomainsFromEnv(),
839
- // Domains saved by the former Facebook "Short Link Domains" setting keep working
840
- // until every environment sets CONNECTOR_SHORT_LINK_DOMAINS.
841
- ...parseShortLinkDomains((_e = this.context.getParameter("ShortLinkDomains")) == null ? void 0 : _e.value)
842
- ],
891
+ allowedHosts: getShortLinkDomainsFromEnv(),
843
892
  resolvedLinksCache: cache.resolved,
844
893
  failedLinks: cache.failed
845
894
  });
@@ -918,7 +967,7 @@ var require_index = __commonJS({
918
967
  onBatch: (batch) => this._writeBatch(writer, batch, fields)
919
968
  });
920
969
  await this._writeBatch(writer, data, fields);
921
- });
970
+ }, nodeName);
922
971
  }
923
972
  }
924
973
  /**
@@ -943,6 +992,16 @@ var require_index = __commonJS({
943
992
  */
944
993
  async _processTimeSeriesNodes(nodes, accounts, state) {
945
994
  if (!nodes.length) return;
995
+ const lagging = nodes.filter(
996
+ (node) => {
997
+ var _a, _b;
998
+ return this.source.getDateStrategy(node.name) !== DATE_STRATEGY.NONE && (((_b = (_a = this.source).getEndLagDays) == null ? void 0 : _b.call(_a, node.name)) ?? 0) > 0;
999
+ }
1000
+ );
1001
+ const dateRange = lagging.length ? this.getDateRange() : null;
1002
+ if (dateRange) {
1003
+ state.cursorCap = lagging.map((node) => this._nodeWindowEnd(node, dateRange)).reduce((earliest, date) => date < earliest ? date : earliest);
1004
+ }
946
1005
  const dayByDayNodes = [];
947
1006
  const windowEndDates = [];
948
1007
  for (const node of nodes) {
@@ -981,6 +1040,12 @@ var require_index = __commonJS({
981
1040
  this.context.log(LOG_LEVEL.WARN, `Could not determine date range for node "${node.name}"`);
982
1041
  return null;
983
1042
  }
1043
+ const endDate = this._nodeWindowEnd(node, dateRange);
1044
+ if (endDate < dateRange.startDate) {
1045
+ this._logLaggedOut(node, dateRange, endDate);
1046
+ return null;
1047
+ }
1048
+ this._logLagCut(node, dateRange, endDate);
984
1049
  const writers = /* @__PURE__ */ new Map();
985
1050
  let completedBy = 0;
986
1051
  this._beginPass(state);
@@ -992,17 +1057,17 @@ var require_index = __commonJS({
992
1057
  fields: node.fields,
993
1058
  accountId: (account == null ? void 0 : account.id) ?? null,
994
1059
  startDate: dateRange.startDate,
995
- endDate: dateRange.endDate
1060
+ endDate
996
1061
  });
997
1062
  await this._writeBatch(writer, data, node.fields);
998
- });
1063
+ }, node.name);
999
1064
  if (done) completedBy += 1;
1000
1065
  }
1001
1066
  if (completedBy === 0 || state.passHardFailures > 0) {
1002
1067
  state.cursorHalted = true;
1003
1068
  return null;
1004
1069
  }
1005
- return dateRange.endDate;
1070
+ return endDate;
1006
1071
  }
1007
1072
  /**
1008
1073
  * DATE_STRATEGY.NONE: the node carries no date window at all, so there is
@@ -1042,8 +1107,15 @@ var require_index = __commonJS({
1042
1107
  return;
1043
1108
  }
1044
1109
  const writers = /* @__PURE__ */ new Map();
1045
- for (const date of this._iterateDates(dateRange.startDate, dateRange.endDate)) {
1110
+ const endDates = new Map(nodes.map((node) => [node, this._nodeWindowEnd(node, dateRange)]));
1111
+ for (const [node, end] of endDates) {
1112
+ if (end < dateRange.startDate) this._logLaggedOut(node, dateRange, end);
1113
+ else this._logLagCut(node, dateRange, end);
1114
+ }
1115
+ const lastDay = [...endDates.values()].reduce((latest, date) => date > latest ? date : latest);
1116
+ for (const date of this._iterateDates(dateRange.startDate, lastDay)) {
1046
1117
  const formattedDate = this._formatDate(date);
1118
+ const dayNodes = nodes.filter((node) => endDates.get(node) >= formattedDate);
1047
1119
  let completedBy = 0;
1048
1120
  this._beginPass(state);
1049
1121
  for (const account of accounts) {
@@ -1052,7 +1124,7 @@ var require_index = __commonJS({
1052
1124
  continue;
1053
1125
  }
1054
1126
  const done = await this._runForAccount(state, account, async () => {
1055
- for (const node of nodes) {
1127
+ for (const node of dayNodes) {
1056
1128
  const writer = this._nodeWriter(writers, node);
1057
1129
  const data = await this.source.fetchData({
1058
1130
  nodeName: node.name,
@@ -1063,7 +1135,7 @@ var require_index = __commonJS({
1063
1135
  });
1064
1136
  await this._writeBatch(writer, data, node.fields);
1065
1137
  }
1066
- });
1138
+ }, formattedDate);
1067
1139
  if (done) completedBy += 1;
1068
1140
  }
1069
1141
  if (completedBy === 0 && state.passHardFailures === 0 && accounts.length > 0) {
@@ -1123,7 +1195,7 @@ var require_index = __commonJS({
1123
1195
  endDate: null
1124
1196
  });
1125
1197
  if (data && data.length) batches.push(data);
1126
- });
1198
+ }, nodeName);
1127
1199
  if (!done) missing.add(this._accountKey(account));
1128
1200
  }
1129
1201
  if (missing.size) {
@@ -1171,6 +1243,29 @@ var require_index = __commonJS({
1171
1243
  );
1172
1244
  }
1173
1245
  await storage.replaceData(snapshot);
1246
+ this._countRows(nodeName, snapshot.length);
1247
+ }
1248
+ /** @private */
1249
+ _countRows(nodeName, count) {
1250
+ if (!this._rowsByNode) return;
1251
+ this._rowsByNode.set(nodeName, (this._rowsByNode.get(nodeName) ?? 0) + count);
1252
+ }
1253
+ /**
1254
+ * Reports 0 rows written for every node that loaded nothing in a completed run. The
1255
+ * storages report only the rows they write, so without this Run History showed such a run
1256
+ * as a plain success with no count at all.
1257
+ *
1258
+ * @param {object[]} nodes the run's planned nodes
1259
+ * @private
1260
+ */
1261
+ _reportEmptyNodes(nodes) {
1262
+ var _a;
1263
+ for (const node of nodes) {
1264
+ if (((_a = this._rowsByNode) == null ? void 0 : _a.get(node.name)) > 0) continue;
1265
+ this.context.emitAnalytics("rows_written", 0, {
1266
+ node: this.source.getDestinationName(node.name, node.schema)
1267
+ });
1268
+ }
1174
1269
  }
1175
1270
  /**
1176
1271
  * One row per unique key, the last one read: what a MERGE keeps. The storages dedupe
@@ -1776,6 +1871,13 @@ var require_index = __commonJS({
1776
1871
  getDateStrategy(nodeName) {
1777
1872
  return DATE_STRATEGY.DAY_BY_DAY;
1778
1873
  }
1874
+ /**
1875
+ * How many days before today a time-series node's window ends, for an API that reports
1876
+ * only completed days. 0: the window ends today.
1877
+ */
1878
+ getEndLagDays(nodeName) {
1879
+ return 0;
1880
+ }
1779
1881
  /**
1780
1882
  * Called after all nodes for an account are processed.
1781
1883
  */
@@ -1908,14 +2010,19 @@ var require_index = __commonJS({
1908
2010
  if (!response.ok) {
1909
2011
  const bodyText = typeof response.text === "function" ? await response.text().catch(() => "") : "";
1910
2012
  const snippet = bodyText ? ` — ${bodyText.slice(0, 300)}` : "";
1911
- const error = new Error(`HTTP ${response.status}: ${response.statusText}${snippet}`);
1912
- error.response = response;
1913
- error.statusCode = response.status;
1914
- error.responseBody = bodyText;
2013
+ let payload;
1915
2014
  try {
1916
- error.payload = JSON.parse(bodyText);
2015
+ payload = JSON.parse(bodyText);
1917
2016
  } catch {
1918
2017
  }
2018
+ const providerMessage = _AbstractSource._providerErrorMessage(payload);
2019
+ const error = new Error(
2020
+ providerMessage ? `HTTP ${response.status}: ${providerMessage}` : `HTTP ${response.status}: ${response.statusText}${snippet}`
2021
+ );
2022
+ error.response = response;
2023
+ error.statusCode = response.status;
2024
+ error.responseBody = bodyText;
2025
+ if (payload !== void 0) error.payload = payload;
1919
2026
  if (await this.isValidToRetry(error) && attempt < totalAttempts - 1) {
1920
2027
  const delay = this.calculateBackoff(attempt, initialDelay);
1921
2028
  this.context.log(
@@ -1954,6 +2061,28 @@ var require_index = __commonJS({
1954
2061
  }
1955
2062
  }
1956
2063
  }
2064
+ /**
2065
+ * The reason a provider gave in its JSON error body, looked up where main's
2066
+ * _extractErrorInfo looked for it, and in a JSON:API error's `detail`. Not its `title`: that
2067
+ * is the same summary for every occurrence, and the body snippet it would replace names the
2068
+ * field in `source.pointer`.
2069
+ *
2070
+ * @param {*} payload - the parsed error body
2071
+ * @returns {string|null} null when the body names no message
2072
+ */
2073
+ static _providerErrorMessage(payload) {
2074
+ var _a;
2075
+ const firstError = Array.isArray(payload == null ? void 0 : payload.errors) ? payload.errors[0] : void 0;
2076
+ const candidates = [
2077
+ (_a = payload == null ? void 0 : payload.error) == null ? void 0 : _a.message,
2078
+ payload == null ? void 0 : payload.message,
2079
+ payload == null ? void 0 : payload.errorMessage,
2080
+ payload == null ? void 0 : payload.error_message,
2081
+ firstError == null ? void 0 : firstError.message,
2082
+ firstError == null ? void 0 : firstError.detail
2083
+ ];
2084
+ return candidates.find((message) => typeof message === "string" && message.trim()) ?? null;
2085
+ }
1957
2086
  /**
1958
2087
  * The fetch options to use for the next redirect hop.
1959
2088
  *
@@ -2321,6 +2450,19 @@ var require_index = __commonJS({
2321
2450
  return fieldsParam.value.split(",").map((field) => field.trim()).filter((field) => field !== "").map((field) => field.split(" ")).filter((field) => field.length === 2).map((field) => field[1]);
2322
2451
  }
2323
2452
  //----------------------------------------------------------------
2453
+ //---- missingSelectedFieldError ----------------------------------
2454
+ /**
2455
+ * The error for a field the Data Mart selects that the connector's schema does not have.
2456
+ * Its usual cause is a connector whose fields changed after the Data Mart was set up.
2457
+ * @param {string} columnName
2458
+ * @returns {Error}
2459
+ */
2460
+ missingSelectedFieldError(columnName) {
2461
+ return new Error(
2462
+ `Field "${columnName}" is selected for import, but the connector does not provide it. If the connector's fields changed after this Data Mart was set up, open Edit Fields in the Data Mart's Input Source and save: a field the connector no longer provides is dropped.`
2463
+ );
2464
+ }
2465
+ //----------------------------------------------------------------
2324
2466
  //---- getColumnType -----------------------------------------------
2325
2467
  /**
2326
2468
  * Get column type for storage from schema
@@ -2817,6 +2959,7 @@ var require_index = __commonJS({
2817
2959
  this._activeErrorHandler = null;
2818
2960
  this._pendingBackoff = null;
2819
2961
  this._tokenCache = /* @__PURE__ */ new Map();
2962
+ this._extractedByNode = /* @__PURE__ */ new Map();
2820
2963
  }
2821
2964
  _compileNodes(nodes = {}) {
2822
2965
  const schema = {};
@@ -2867,8 +3010,13 @@ var require_index = __commonJS({
2867
3010
  getDateStrategy(nodeName) {
2868
3011
  return nodeDateStrategy(this.model.nodes[nodeName]);
2869
3012
  }
3013
+ getEndLagDays(nodeName) {
3014
+ const node = this.model.nodes[nodeName];
3015
+ if (nodeDateStrategy(node) === DATE_STRATEGY.NONE) return 0;
3016
+ return node.incremental.endLagDays ?? 0;
3017
+ }
2870
3018
  async fetchData({ nodeName, fields, accountId, startDate, endDate }) {
2871
- var _a, _b, _c, _d;
3019
+ var _a, _b, _c, _d, _e;
2872
3020
  const node = this.model.nodes[nodeName];
2873
3021
  if (!node) throw new Error(`DeclarativeSource: unknown node "${nodeName}"`);
2874
3022
  const scope = this._scope({ accountId, startDate, endDate, fields });
@@ -2936,8 +3084,12 @@ var require_index = __commonJS({
2936
3084
  this.runLimits
2937
3085
  );
2938
3086
  const rawRecords = await retriever.run(scope);
3087
+ const nodeTag = ((_d = this.context.getParameter("DestinationTableName")) == null ? void 0 : _d.value) ?? nodeName;
3088
+ this._extractedByNode.set(nodeName, {
3089
+ tag: nodeTag,
3090
+ count: (((_e = this._extractedByNode.get(nodeName)) == null ? void 0 : _e.count) ?? 0) + rawRecords.length
3091
+ });
2939
3092
  if (rawRecords.length > 0) {
2940
- const nodeTag = ((_d = this.context.getParameter("DestinationTableName")) == null ? void 0 : _d.value) ?? nodeName;
2941
3093
  this.context.emitAnalytics("rows_extracted", rawRecords.length, { node: nodeTag });
2942
3094
  }
2943
3095
  const recordFilter = node.recordFilter ? new RecordFilter(node.recordFilter) : null;
@@ -2952,6 +3104,37 @@ var require_index = __commonJS({
2952
3104
  }
2953
3105
  return this._selectFields(new FieldCaster(node.fields || {}).cast(records), node, fields);
2954
3106
  }
3107
+ /**
3108
+ * Names each node whose requests got no records in the whole run. A record path that
3109
+ * misses the data or a wrong parameter value completes like a run with nothing new, so this
3110
+ * is the only hint the run leaves. INFO, not WARN: an API with nothing for the window is
3111
+ * also a normal outcome.
3112
+ */
3113
+ onImportComplete() {
3114
+ var _a;
3115
+ for (const [nodeName, { tag, count }] of this._extractedByNode) {
3116
+ if (count > 0) continue;
3117
+ this.context.emitAnalytics("rows_extracted", 0, { node: tag });
3118
+ this.context.log(
3119
+ LOG_LEVEL.INFO,
3120
+ `No records came back for node "${nodeName}" in this run. If you expected data, check the node's record path, and the parameters and dates its request uses.` + DeclarativeSource._partitionHint((_a = this.model.nodes[nodeName]) == null ? void 0 : _a.partitionRouter)
3121
+ );
3122
+ }
3123
+ }
3124
+ /**
3125
+ * A partitioned node sends one request per slice, and none when there are no slices, so
3126
+ * its own request is not where an empty run starts. Named as the builder's Partition form
3127
+ * names the fields.
3128
+ */
3129
+ static _partitionHint(partitionRouter) {
3130
+ if ((partitionRouter == null ? void 0 : partitionRouter.type) === "substream") {
3131
+ return " Its requests run once per parent record, so also check Parent record path and Parent key: when they find nothing, the node sends no requests.";
3132
+ }
3133
+ if ((partitionRouter == null ? void 0 : partitionRouter.type) === "list") {
3134
+ return " Its requests run once per value of its list, so also check Values or Values from parameter: an empty list sends no requests.";
3135
+ }
3136
+ return "";
3137
+ }
2955
3138
  /**
2956
3139
  * Keeps the fields the Data Mart selected, and the node's unique keys, which the storage
2957
3140
  * merges on. A bundled source reads only the selected fields; casting every declared one
@@ -2982,9 +3165,10 @@ var require_index = __commonJS({
2982
3165
  async isValidToRetry(error) {
2983
3166
  const handler = this._activeErrorHandler;
2984
3167
  if (!handler) {
2985
- this._pendingBackoff = null;
2986
3168
  error._declAction = null;
2987
- return this._defaultRetryable(error);
3169
+ const retry2 = this._defaultRetryable(error);
3170
+ this._pendingBackoff = retry2 ? { filter: null, response: error.response } : null;
3171
+ return retry2;
2988
3172
  }
2989
3173
  let bodyText = "";
2990
3174
  let bodyJson = null;
@@ -3003,13 +3187,23 @@ var require_index = __commonJS({
3003
3187
  return retry;
3004
3188
  }
3005
3189
  calculateBackoff(attempt, initialDelay) {
3006
- if (this._pendingBackoff && this._activeErrorHandler) {
3007
- const { filter, response } = this._pendingBackoff;
3008
- this._pendingBackoff = null;
3009
- const ms = this._activeErrorHandler.delayMs(filter, response, attempt, initialDelay);
3190
+ const pending = this._pendingBackoff;
3191
+ this._pendingBackoff = null;
3192
+ if (pending && this._activeErrorHandler) {
3193
+ const ms = this._activeErrorHandler.delayMs(
3194
+ pending.filter,
3195
+ pending.response,
3196
+ attempt,
3197
+ initialDelay
3198
+ );
3010
3199
  if (ms != null) return ms;
3011
3200
  }
3012
- return super.calculateBackoff(attempt, initialDelay);
3201
+ const fallback = super.calculateBackoff(attempt, initialDelay);
3202
+ if (pending && !process.env.OW_TEST) {
3203
+ const ms = retryAfterDelayMs(pending.response);
3204
+ if (ms != null) return Math.max(ms, fallback);
3205
+ }
3206
+ return fallback;
3013
3207
  }
3014
3208
  /**
3015
3209
  * A 401 or 403 skips the account as refused access, but not one a filter mapped to FAIL:
@@ -3220,6 +3414,16 @@ var require_index = __commonJS({
3220
3414
  if (!Number.isFinite(ms)) return null;
3221
3415
  return Math.min(Math.max(0, ms), MAX_HEADER_RETRY_DELAY_MS);
3222
3416
  }
3417
+ function retryAfterDelayMs(response, headerName = "Retry-After") {
3418
+ var _a, _b;
3419
+ const raw = (_b = (_a = response == null ? void 0 : response.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, headerName);
3420
+ if (raw == null) return null;
3421
+ const secs = Number(raw);
3422
+ if (Number.isFinite(secs)) return clampHeaderDelay(secs * 1e3);
3423
+ const dateMs = Date.parse(raw);
3424
+ if (!Number.isNaN(dateMs)) return clampHeaderDelay(dateMs - Date.now());
3425
+ return null;
3426
+ }
3223
3427
  class ErrorHandler {
3224
3428
  /**
3225
3429
  * @param {{ responseFilters?: Array<object>, backoff?: object }} config
@@ -3290,16 +3494,7 @@ var require_index = __commonJS({
3290
3494
  }
3291
3495
  }
3292
3496
  _waitTimeFromHeader(spec, response) {
3293
- var _a, _b;
3294
- if (!response) return null;
3295
- const headerName = spec && spec.header || "Retry-After";
3296
- const raw = (_b = (_a = response.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, headerName);
3297
- if (raw == null) return null;
3298
- const secs = Number(raw);
3299
- if (Number.isFinite(secs)) return clampHeaderDelay(secs * 1e3);
3300
- const dateMs = Date.parse(raw);
3301
- if (!Number.isNaN(dateMs)) return clampHeaderDelay(dateMs - Date.now());
3302
- return null;
3497
+ return retryAfterDelayMs(response, spec && spec.header || "Retry-After");
3303
3498
  }
3304
3499
  _waitUntilTimeFromHeader(spec, response) {
3305
3500
  var _a, _b;
@@ -3497,6 +3692,45 @@ var require_index = __commonJS({
3497
3692
  const PAGINATION_TYPES = /* @__PURE__ */ new Set(["none", "offset", "page", "cursor"]);
3498
3693
  const INJECT_TARGETS = /* @__PURE__ */ new Set(["query", "header", "body", "path"]);
3499
3694
  const RESPONSE_FORMATS = /* @__PURE__ */ new Set(["json", "csv", "jsonl"]);
3695
+ const MAX_END_LAG_DAYS = 30;
3696
+ const DATE_WINDOW_PLACEHOLDER = /\{\{\s*(dateWindow(?:\.[a-zA-Z0-9_]+)*)\s*\}\}/;
3697
+ function dateWindowTemplateIn(value) {
3698
+ var _a;
3699
+ if (typeof value === "string") return ((_a = DATE_WINDOW_PLACEHOLDER.exec(value)) == null ? void 0 : _a[1]) ?? null;
3700
+ if (value && typeof value === "object") {
3701
+ for (const item of Object.values(value)) {
3702
+ const path = dateWindowTemplateIn(item);
3703
+ if (path) return path;
3704
+ }
3705
+ }
3706
+ return null;
3707
+ }
3708
+ function assertNoStrictDateWindowTemplate(nodeName, node) {
3709
+ var _a, _b, _c;
3710
+ const isAsync = ((_a = node.retriever) == null ? void 0 : _a.type) === "async";
3711
+ for (const [where, spec] of [
3712
+ // An async node sends its retriever's requests, never `request`.
3713
+ ["request", isAsync ? void 0 : node.request],
3714
+ ["retriever.submit", isAsync ? node.retriever.submit : void 0],
3715
+ ["retriever.poll", isAsync ? node.retriever.poll : void 0],
3716
+ ["partitionRouter.parent.request", (_c = (_b = node.partitionRouter) == null ? void 0 : _b.parent) == null ? void 0 : _c.request]
3717
+ ]) {
3718
+ if (!spec || typeof spec !== "object") continue;
3719
+ const sendsBody = String(spec.method || "GET").toUpperCase() !== "GET";
3720
+ for (const [place, value] of [
3721
+ [`${where}.path`, spec.path],
3722
+ [`${where}.body`, sendsBody ? spec.body : void 0]
3723
+ ]) {
3724
+ const path = dateWindowTemplateIn(value);
3725
+ if (path) {
3726
+ const fix = node.isFullRefresh ? 'Remove "isFullRefresh" and add an "incremental" block to fetch by date window, or remove the template.' : 'Add an "incremental" block to the node, or remove the template.';
3727
+ throw new Error(
3728
+ `ManifestParser: node "${nodeName}" uses "{{ ${path} }}" in ${place}, but the node is never given a date window: only a node with an "incremental" strategy, and without "isFullRefresh", is fetched one window at a time. ${fix}`
3729
+ );
3730
+ }
3731
+ }
3732
+ }
3733
+ }
3500
3734
  const LEGACY_INJECT_PARAM = {
3501
3735
  cursor: "cursorParam",
3502
3736
  offset: "offsetParam",
@@ -3620,10 +3854,12 @@ var require_index = __commonJS({
3620
3854
  class ManifestParser {
3621
3855
  /**
3622
3856
  * @param {string} json
3857
+ * @param {{ authoring?: boolean }} [options] - `authoring` adds the checks for a manifest
3858
+ * being published or tested, which a stored version must not start failing on.
3623
3859
  * @returns {object} ManifestModel
3624
3860
  */
3625
- parse(json) {
3626
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
3861
+ parse(json, { authoring = false } = {}) {
3862
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A;
3627
3863
  let raw;
3628
3864
  try {
3629
3865
  raw = JSON.parse(json);
@@ -3799,11 +4035,27 @@ var require_index = __commonJS({
3799
4035
  `ManifestParser: node "${nodeName}" has unknown incremental.strategy "${strategy}"`
3800
4036
  );
3801
4037
  }
4038
+ const endLagDays = (_w = node.incremental) == null ? void 0 : _w.endLagDays;
4039
+ if (endLagDays !== void 0) {
4040
+ if (!Number.isInteger(endLagDays) || endLagDays < 0 || endLagDays > MAX_END_LAG_DAYS) {
4041
+ throw new Error(
4042
+ `ManifestParser: node "${nodeName}" incremental.endLagDays must be a whole number of days from 0 to ${MAX_END_LAG_DAYS}, e.g. 1 to leave out today`
4043
+ );
4044
+ }
4045
+ if (strategy === "none") {
4046
+ throw new Error(
4047
+ `ManifestParser: node "${nodeName}" incremental.endLagDays needs a "day-by-day" or "range" strategy — a node with no date window has no window end to move`
4048
+ );
4049
+ }
4050
+ }
3802
4051
  if (declaresDateWindow(node) && node.isFullRefresh) {
3803
4052
  throw new Error(
3804
4053
  `ManifestParser: node "${nodeName}" declares both "incremental" and "isFullRefresh", which are mutually exclusive — a full-refresh node replaces the whole table and is never given a date window, so the incremental block would be silently ignored. Remove "isFullRefresh" to fetch by date window, or remove the "incremental" block.`
3805
4054
  );
3806
4055
  }
4056
+ if (authoring && !declaresDateWindow(node)) {
4057
+ assertNoStrictDateWindowTemplate(nodeName, node);
4058
+ }
3807
4059
  if (node.transformations !== void 0) {
3808
4060
  if (!Array.isArray(node.transformations)) {
3809
4061
  throw new Error(`ManifestParser: node "${nodeName}" transformations must be an array`);
@@ -3899,7 +4151,7 @@ var require_index = __commonJS({
3899
4151
  }
3900
4152
  }
3901
4153
  if (eh.backoff !== void 0) validateBackoff(eh.backoff, nodeName, "errorHandler");
3902
- if (((_w = node.retriever) == null ? void 0 : _w.type) === "async") {
4154
+ if (((_x = node.retriever) == null ? void 0 : _x.type) === "async") {
3903
4155
  throw new Error(
3904
4156
  `ManifestParser: node "${nodeName}" errorHandler is not supported with an async retriever — an async node paces itself through "retriever.poll.backoff" instead, so these filters and this backoff would never run. Remove "errorHandler", or make the node sync.`
3905
4157
  );
@@ -3952,7 +4204,7 @@ var require_index = __commonJS({
3952
4204
  );
3953
4205
  }
3954
4206
  }
3955
- if (((_x = node.retriever) == null ? void 0 : _x.type) === "async") {
4207
+ if (((_y = node.retriever) == null ? void 0 : _y.type) === "async") {
3956
4208
  throw new Error(
3957
4209
  `ManifestParser: node "${nodeName}" partitionRouter is not supported with an async retriever`
3958
4210
  );
@@ -3998,7 +4250,7 @@ var require_index = __commonJS({
3998
4250
  }
3999
4251
  }
4000
4252
  }
4001
- const rType = ((_y = node.retriever) == null ? void 0 : _y.type) ?? "sync";
4253
+ const rType = ((_z = node.retriever) == null ? void 0 : _z.type) ?? "sync";
4002
4254
  if (rType === "sync") {
4003
4255
  if (!node.request) {
4004
4256
  throw new Error(`ManifestParser: node "${nodeName}" is missing "request"`);
@@ -4130,7 +4382,7 @@ var require_index = __commonJS({
4130
4382
  );
4131
4383
  }
4132
4384
  for (const key of ["maxAttempts", "initialMs", "maxMs"]) {
4133
- const value = (_z = r.poll.backoff) == null ? void 0 : _z[key];
4385
+ const value = (_A = r.poll.backoff) == null ? void 0 : _A[key];
4134
4386
  if (value === void 0) continue;
4135
4387
  const valid = typeof value === "number" && value > 0 && (key === "maxAttempts" ? Number.isInteger(value) : Number.isFinite(value));
4136
4388
  if (!valid) {
@@ -4209,7 +4461,7 @@ var require_index = __commonJS({
4209
4461
  const identifier = /^[A-Za-z_][A-Za-z0-9_]*$/;
4210
4462
  if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(nodeName)) {
4211
4463
  throw new Error(
4212
- `ManifestParser: node "${nodeName}" must start with a letter and contain only letters, digits and underscores`
4464
+ `ManifestParser: node "${nodeName}" must start with a letter and contain only letters, digits and underscores` + (nodeName.includes("/") ? `; the API path goes in the node's request.path` : "")
4213
4465
  );
4214
4466
  }
4215
4467
  if (node.destinationName !== void 0 && !identifier.test(String(node.destinationName))) {
@@ -5029,6 +5281,9 @@ var require_index = __commonJS({
5029
5281
  ];
5030
5282
  const CONCLUSIVE_DNS_FAILURES = /* @__PURE__ */ new Set(["ENOTFOUND", "ENODATA"]);
5031
5283
  const INCONCLUSIVE_LOOKUP_ATTEMPTS = 3;
5284
+ function refusal(message) {
5285
+ return Object.assign(new Error(message), { isRefusal: true });
5286
+ }
5032
5287
  class SsrfGuard {
5033
5288
  /**
5034
5289
  * True if `ip` is a private/loopback/link-local address. Accepts both the
@@ -5138,11 +5393,11 @@ var require_index = __commonJS({
5138
5393
  }
5139
5394
  if (SsrfGuard._allowLocalEgress()) return;
5140
5395
  if (url.protocol !== "https:") {
5141
- throw new Error(`SsrfGuard: URL must use https, got "${url.protocol}"`);
5396
+ throw refusal(`SsrfGuard: URL must use https, got "${url.protocol}"`);
5142
5397
  }
5143
5398
  const host = url.hostname.toLowerCase().replace(/\.$/, "");
5144
5399
  if (this._isBlockedLiteralHost(host)) {
5145
- throw new Error(`SsrfGuard: blocked IP/host "${host}"`);
5400
+ throw refusal(`SsrfGuard: blocked IP/host "${host}"`);
5146
5401
  }
5147
5402
  await this._assertResolvesToPublic(host);
5148
5403
  }
@@ -5192,7 +5447,7 @@ var require_index = __commonJS({
5192
5447
  for (const a of list) {
5193
5448
  const address = a && typeof a === "object" ? a.address : a;
5194
5449
  if (SsrfGuard.isBlockedIp(address)) {
5195
- throw new Error(`SsrfGuard: host "${host}" resolves to blocked address "${address}"`);
5450
+ throw refusal(`SsrfGuard: host "${host}" resolves to blocked address "${address}"`);
5196
5451
  }
5197
5452
  }
5198
5453
  }
@@ -6626,25 +6881,28 @@ var require_index = __commonJS({
6626
6881
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
6627
6882
  }
6628
6883
  };
6629
- async function resolveShortLinkFields(data, specs, { nestedPathHosts = [], resolvedLinksCache = /* @__PURE__ */ new Map(), failedLinks = /* @__PURE__ */ new Set() } = {}) {
6884
+ async function resolveShortLinkFields(data, specs, { allowedHosts = [], resolvedLinksCache = /* @__PURE__ */ new Map(), failedLinks = /* @__PURE__ */ new Set() } = {}) {
6630
6885
  if (!Array.isArray(data) || data.length === 0 || !Array.isArray(specs) || specs.length === 0) return data;
6631
- const candidates = _collectCandidateUrls(data, specs, nestedPathHosts);
6886
+ const candidates = _collectCandidateUrls(data, specs, allowedHosts);
6632
6887
  const hasPlainTargets = specs.some((spec) => !spec.urlKey);
6633
6888
  if (candidates.length === 0 && !hasPlainTargets) return data;
6634
6889
  const uncachedLinks = candidates.filter((url) => !resolvedLinksCache.has(url)).map((originalUrl) => ({ originalUrl }));
6635
- const freshlyResolved = await _resolveShortLinks(uncachedLinks);
6890
+ const freshlyResolved = await _resolveShortLinks(uncachedLinks, allowedHosts);
6636
6891
  freshlyResolved.forEach((link) => {
6637
6892
  resolvedLinksCache.set(link.originalUrl, link.resolvedUrl);
6638
6893
  if (!link.ok) failedLinks.add(link.originalUrl);
6639
6894
  });
6640
- return data.map((record) => specs.reduce((current, spec) => _applySpec(current, spec, resolvedLinksCache), record));
6895
+ const resolvable = new Set(candidates);
6896
+ return data.map(
6897
+ (record) => specs.reduce((current, spec) => _applySpec(current, spec, resolvedLinksCache, resolvable), record)
6898
+ );
6641
6899
  }
6642
- function _collectCandidateUrls(data, specs, nestedPathHosts) {
6900
+ function _collectCandidateUrls(data, specs, allowedHosts) {
6643
6901
  const unique = /* @__PURE__ */ new Set();
6644
6902
  data.forEach((record) => {
6645
6903
  specs.forEach((spec) => {
6646
6904
  _readUrls(record[spec.field], spec).forEach((url) => {
6647
- if (_isPotentialShortLink(url, nestedPathHosts)) unique.add(url);
6905
+ if (_isPotentialShortLink(url, allowedHosts)) unique.add(url);
6648
6906
  });
6649
6907
  });
6650
6908
  });
@@ -6669,9 +6927,9 @@ var require_index = __commonJS({
6669
6927
  }
6670
6928
  return null;
6671
6929
  }
6672
- function _applySpec(record, spec, cache) {
6930
+ function _applySpec(record, spec, cache, resolvable) {
6673
6931
  const value = record[spec.field];
6674
- const resolve = (url) => typeof url === "string" && cache.has(url) ? cache.get(url) : url;
6932
+ const resolve = (url) => typeof url === "string" && resolvable.has(url) && cache.has(url) ? cache.get(url) : url;
6675
6933
  if (spec.urlKey) {
6676
6934
  const original = value && value[spec.urlKey];
6677
6935
  const resolved = resolve(original);
@@ -6696,7 +6954,15 @@ var require_index = __commonJS({
6696
6954
  }
6697
6955
  function parseShortLinkDomains(value) {
6698
6956
  if (!value) return [];
6699
- return String(value).split(/[,;\s]+/).map((entry) => entry.replace(/^[a-z]+:\/\//i, "").split("/")[0].split(":")[0].replace(/\.$/, "").trim().toLowerCase()).filter((host) => host.includes("."));
6957
+ return String(value).split(/[,;\s]+/).map((entry) => entry.replace(/^[a-z]+:\/\//i, "").replace(/^\*?\./, "").split("/")[0].split(":")[0].replace(/\.$/, "").trim().toLowerCase()).map(_toAsciiHost).filter((host) => host.includes(".") && !/^\d+(\.\d+){3}$/.test(host));
6958
+ }
6959
+ function _toAsciiHost(host) {
6960
+ if (!host) return "";
6961
+ try {
6962
+ return new URL(`https://${host}`).hostname;
6963
+ } catch (_error) {
6964
+ return "";
6965
+ }
6700
6966
  }
6701
6967
  const SHORT_LINKS_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
6702
6968
  const SHORT_LINKS_STATE_MAX_BYTES = 24 * 1024;
@@ -6725,60 +6991,94 @@ var require_index = __commonJS({
6725
6991
  return state;
6726
6992
  }
6727
6993
  const UTM_PARAM_PATTERN = /utm_(source|medium|campaign|term|content)/;
6728
- function _isPotentialShortLink(url, nestedPathHosts) {
6994
+ const KNOWN_SHORT_LINK_DOMAINS = [
6995
+ "bit.ly",
6996
+ "tinyurl.com",
6997
+ "t.co",
6998
+ "lnkd.in",
6999
+ "youtu.be",
7000
+ "amzn.to",
7001
+ "ow.ly",
7002
+ "buff.ly",
7003
+ "cutt.ly",
7004
+ "is.gd",
7005
+ "rebrand.ly"
7006
+ ];
7007
+ function _isPotentialShortLink(url, allowedHosts) {
6729
7008
  if (!url || typeof url !== "string") return false;
6730
7009
  try {
6731
7010
  const parsedUrl = new URL(url);
6732
7011
  if (parsedUrl.protocol !== "https:" || url.includes("?") || UTM_PARAM_PATTERN.test(url)) {
6733
7012
  return false;
6734
7013
  }
6735
- const isConfiguredHost = _isNestedPathHost(parsedUrl.hostname, nestedPathHosts);
6736
- const pathSegments = _getPathSegments(parsedUrl.pathname, isConfiguredHost);
6737
- if (!pathSegments.every(Boolean)) return false;
6738
- return pathSegments.length === 1 || isConfiguredHost;
7014
+ if (!_isAllowedShortLinkHost(parsedUrl.hostname, allowedHosts)) return false;
7015
+ const pathSegments = _getPathSegments(parsedUrl.pathname);
7016
+ return pathSegments.every(Boolean);
6739
7017
  } catch (_error) {
6740
7018
  return false;
6741
7019
  }
6742
7020
  }
6743
- function _getPathSegments(pathname, allowTrailingSlash) {
7021
+ function _getPathSegments(pathname) {
6744
7022
  const segments = pathname.slice(1).split("/");
6745
7023
  const hasTrailingSlash = segments.length > 1 && segments[segments.length - 1] === "";
6746
- return allowTrailingSlash && hasTrailingSlash ? segments.slice(0, -1) : segments;
7024
+ return hasTrailingSlash ? segments.slice(0, -1) : segments;
6747
7025
  }
6748
- function _isNestedPathHost(hostname, nestedPathHosts) {
6749
- return nestedPathHosts.some((host) => hostname === host || hostname.endsWith(`.${host}`));
7026
+ function _isAllowedShortLinkHost(hostname, allowedHosts) {
7027
+ const isMatch = (host) => hostname === host || hostname.endsWith(`.${host}`);
7028
+ return KNOWN_SHORT_LINK_DOMAINS.some(isMatch) || allowedHosts.some(isMatch);
6750
7029
  }
6751
7030
  const SHORT_LINK_FETCH_TIMEOUT_MS = 1e4;
6752
7031
  const SHORT_LINK_MAX_REDIRECTS = 20;
6753
7032
  const SHORT_LINK_CONCURRENCY = 10;
6754
- async function _resolveShortLinks(shortLinks) {
7033
+ async function _resolveShortLinks(shortLinks, allowedHosts = []) {
6755
7034
  const results = [];
6756
7035
  for (let i = 0; i < shortLinks.length; i += SHORT_LINK_CONCURRENCY) {
6757
7036
  const batch = shortLinks.slice(i, i + SHORT_LINK_CONCURRENCY);
6758
- results.push(...await Promise.all(batch.map(_resolveShortLink)));
7037
+ results.push(...await Promise.all(batch.map((link) => _resolveShortLink(link, allowedHosts))));
6759
7038
  }
6760
7039
  return results;
6761
7040
  }
6762
- async function _resolveShortLink(linkObj) {
7041
+ async function _resolveShortLink(linkObj, allowedHosts = []) {
6763
7042
  const originalUrl = linkObj.originalUrl;
6764
7043
  try {
6765
- return { originalUrl, resolvedUrl: await _followRedirects(originalUrl), ok: true };
7044
+ return { originalUrl, resolvedUrl: await _followRedirects(originalUrl, allowedHosts), ok: true };
6766
7045
  } catch (error) {
6767
- console.log(`Failed to resolve short link ${originalUrl}: ${error.message}`);
6768
- return { originalUrl, resolvedUrl: originalUrl, ok: false };
7046
+ console.log(`Failed to resolve short link ${_describeUrl(originalUrl)}: ${error.message}`);
7047
+ return { originalUrl, resolvedUrl: originalUrl, ok: Boolean(error.isRefusal) };
7048
+ }
7049
+ }
7050
+ function _describeUrl(url) {
7051
+ try {
7052
+ const { origin, pathname } = new URL(url);
7053
+ return `${origin}${pathname}`;
7054
+ } catch (_error) {
7055
+ return "[unparseable URL]";
6769
7056
  }
6770
7057
  }
6771
- async function _followRedirects(startUrl) {
7058
+ const SHORT_LINK_TRANSIENT_STATUS = (status) => status === 408 || status === 429 || status >= 500;
7059
+ async function _followRedirects(startUrl, allowedHosts = []) {
7060
+ var _a;
6772
7061
  let currentUrl = startUrl;
6773
7062
  for (let hop = 0; hop <= SHORT_LINK_MAX_REDIRECTS; hop++) {
6774
7063
  if (!_isPublicHttpUrl(currentUrl)) {
6775
- throw new Error(`Refusing to request non-public URL ${currentUrl}`);
7064
+ const refusal2 = new Error(`Refusing non-public URL ${_describeUrl(currentUrl)}`);
7065
+ refusal2.isRefusal = true;
7066
+ throw refusal2;
6776
7067
  }
7068
+ if (hop > 0 && !_isAllowedShortLinkHost(new URL(currentUrl).hostname, allowedHosts)) {
7069
+ return currentUrl;
7070
+ }
7071
+ await new SsrfGuard([]).assertPublicHttps(currentUrl);
6777
7072
  const response = await fetch(currentUrl, {
6778
7073
  method: "GET",
6779
7074
  redirect: "manual",
6780
7075
  signal: AbortSignal.timeout(SHORT_LINK_FETCH_TIMEOUT_MS)
6781
7076
  });
7077
+ await ((_a = response.body) == null ? void 0 : _a.cancel().catch(() => {
7078
+ }));
7079
+ if (SHORT_LINK_TRANSIENT_STATUS(response.status)) {
7080
+ throw new Error(`Short link service answered ${response.status}`);
7081
+ }
6782
7082
  const location = _getRedirectLocation(response);
6783
7083
  if (!location) return currentUrl;
6784
7084
  currentUrl = new URL(location, currentUrl).toString();
@@ -7087,7 +7387,7 @@ var require_index = __commonJS({
7087
7387
  let columnName = tableColumns[i];
7088
7388
  let columnDescription = "";
7089
7389
  if (!(columnName in this.schema)) {
7090
- throw new Error(`Required field ${columnName} not found in schema`);
7390
+ throw this.missingSelectedFieldError(columnName);
7091
7391
  }
7092
7392
  let columnType = this.getColumnType(columnName);
7093
7393
  if ("description" in this.schema[columnName]) {
@@ -7715,7 +8015,7 @@ ${columnDef}`);
7715
8015
  let columnName = tableColumns[i];
7716
8016
  let columnDescription = "";
7717
8017
  if (!(columnName in this.schema)) {
7718
- throw new Error(`Required field ${columnName} not found in schema`);
8018
+ throw this.missingSelectedFieldError(columnName);
7719
8019
  }
7720
8020
  let columnType = this.getColumnType(columnName);
7721
8021
  if ("description" in this.schema[columnName]) {
@@ -8741,7 +9041,7 @@ COMMIT TRANSACTION;`;
8741
9041
  let columnName = tableColumns[i];
8742
9042
  let columnDescription = "";
8743
9043
  if (!(columnName in this.schema)) {
8744
- throw new Error(`Required field ${columnName} not found in schema`);
9044
+ throw this.missingSelectedFieldError(columnName);
8745
9045
  }
8746
9046
  let columnType = this.getColumnType(columnName);
8747
9047
  const isPartOfPK = pkColumns.includes(columnName);
@@ -12344,7 +12644,7 @@ Query: ${query}`);
12344
12644
  requiredType: "boolean",
12345
12645
  default: true,
12346
12646
  label: "Process Short Links",
12347
- description: "Resolve short links in landing URL fields to their landing page. The resolved value is written to the matching _parsed field (for example website_url_parsed)",
12647
+ description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example website_url_parsed)",
12348
12648
  attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
12349
12649
  },
12350
12650
  CreateEmptyTables: {
@@ -14336,7 +14636,7 @@ Query: ${query}`);
14336
14636
  requiredType: "boolean",
14337
14637
  default: true,
14338
14638
  label: "Process Short Links",
14339
- description: "Resolve short links in landing URL fields to their landing page. The resolved value is written to the matching _parsed field (for example landing_page_url_parsed)",
14639
+ description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example landing_page_url_parsed)",
14340
14640
  attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
14341
14641
  },
14342
14642
  CreateEmptyTables: {
@@ -19970,7 +20270,7 @@ Query: ${query}`);
19970
20270
  requiredType: "boolean",
19971
20271
  default: true,
19972
20272
  label: "Process Short Links",
19973
- description: "Resolve short links in landing URL fields to their landing page. The resolved value is written to the matching _parsed field (for example click_url_parsed)",
20273
+ description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example click_url_parsed)",
19974
20274
  attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
19975
20275
  },
19976
20276
  CreateEmptyTables: {
@@ -22147,7 +22447,7 @@ API Response: ${JSON.stringify(statusResult, null, 2)}`);
22147
22447
  requiredType: "boolean",
22148
22448
  default: true,
22149
22449
  label: "Process Short Links",
22150
- description: "Resolve short links in landing URL fields to their landing page. The resolved value is written to the matching _parsed field (for example FinalUrlParsed)",
22450
+ description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example FinalUrlParsed)",
22151
22451
  attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
22152
22452
  },
22153
22453
  CreateEmptyTables: {
@@ -26850,7 +27150,7 @@ API Response: ${JSON.stringify(statusResult, null, 2)}`);
26850
27150
  requiredType: "boolean",
26851
27151
  default: true,
26852
27152
  label: "Process Short Links",
26853
- description: "Resolve short links in landing URL fields to their landing page. The resolved value is written to the matching _parsed field (for example ad_final_urls_parsed)",
27153
+ description: "Resolve short links in landing URL fields to the address their short link service points to. The resolved value is written to the matching _parsed field (for example ad_final_urls_parsed)",
26854
27154
  attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
26855
27155
  },
26856
27156
  CreateEmptyTables: {
@@ -34399,7 +34699,7 @@ API Response: ${JSON.stringify(statusResult, null, 2)}`);
34399
34699
  requiredType: "boolean",
34400
34700
  default: true,
34401
34701
  label: "Process Short Links",
34402
- description: "Resolve short links to their landing page: link_url_asset.parsed_url on the Ad Insights by Link URL Asset endpoint, and link_url_parsed / object_url_parsed on Ad Creatives",
34702
+ description: "Resolve short links to the address their short link service points to: link_url_asset.parsed_url on the Ad Insights by Link URL Asset endpoint, and link_url_parsed / object_url_parsed on Ad Creatives",
34403
34703
  attributes: [CONFIG_ATTRIBUTES2.ADVANCED]
34404
34704
  },
34405
34705
  CreateEmptyTables: {