iobroker.parcelapp 0.7.2 → 0.9.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.
package/README.md CHANGED
@@ -34,7 +34,7 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
34
34
  ## Requirements
35
35
 
36
36
  - **Node.js >= 22**
37
- - **ioBroker js-controller >= 7.1.2**
37
+ - **ioBroker js-controller >= 7.2.2**
38
38
  - **ioBroker Admin >= 7.8.23**
39
39
  - **parcel.app Premium subscription** — required for API access
40
40
 
@@ -87,10 +87,13 @@ sendTo("parcelapp.0", "addDelivery", {
87
87
  tracking_number: "1234567890",
88
88
  carrier_code: "dhl",
89
89
  description: "My package",
90
+ // optional:
91
+ language: "de", // tracking language as an ISO 639-1 code, default "en"
92
+ send_push_confirmation: true, // send a push once the delivery is added, default false
90
93
  });
91
94
  ```
92
95
 
93
- The delivery is added to your parcel.app account and immediately appears in ioBroker after an automatic poll.
96
+ `tracking_number`, `carrier_code` and `description` are required; `language` and `send_push_confirmation` are optional. The delivery is added to your parcel.app account and a poll is triggered right away but freshly added deliveries usually have no tracking data yet (see the note below).
94
97
 
95
98
  **Notes:**
96
99
 
@@ -126,7 +129,17 @@ The delivery is added to your parcel.app account and immediately appears in ioBr
126
129
  Placeholder for the next version (at the beginning of the line):
127
130
  ### **WORK IN PROGRESS**
128
131
  -->
129
- ### 0.7.2 (2026-06-12)
132
+ ### 0.9.0 (2026-06-23)
133
+
134
+ - Fixed: tracked packages could disappear from the object tree after a temporary update error or an unexpected API response — a package is now kept until parcel.app actually stops returning it.
135
+ - Changed: multi-day delivery windows now show the date on each side (e.g. `12-06 14:30 - 12-08 18:30`) instead of looking same-day; out-of-range or reversed dates no longer produce a misleading window.
136
+
137
+ ### 0.8.0 (2026-06-19)
138
+
139
+ - The delivery window is now also shown for carriers that report it only as a date/time range, not just when the API provides a Unix timestamp.
140
+ - When adding a delivery via script, you can now set an optional tracking language and request a push confirmation.
141
+
142
+ ### 0.7.2 (2026-06-12) — stable
130
143
 
131
144
  - Much quieter state updates: a package's last-updated timestamp now only changes when its tracking data actually changed, and device entries are no longer rewritten on every poll
132
145
  - Adding a delivery with a malformed request now returns a clear error message instead of failing cryptically
@@ -139,16 +152,6 @@ The delivery is added to your parcel.app account and immediately appears in ioBr
139
152
 
140
153
  - Added optional Sentry error reporting: crashes are sent to the developer so issues get fixed faster. Active only with ioBroker diagnostics enabled; anonymous.
141
154
 
142
- ### 0.6.0 (2026-05-31)
143
-
144
- - The summary delivery window now covers the full time range when several packages are expected the same day — previously an overlapping window could be cut short.
145
- - Packages reported with an unrecognized status are no longer mistaken for delivered and removed; they stay visible as "Unknown".
146
- - A delivery added via the admin button now appears immediately instead of only after the next polling cycle.
147
-
148
- ### 0.5.3 (2026-05-23) — stable
149
-
150
- - Reduced unnecessary state-change events by skipping writes when the value has not changed.
151
-
152
155
  [Older changelogs can be found there](CHANGELOG_OLD.md)
153
156
 
154
157
  ## Support
@@ -21,7 +21,8 @@ __export(coerce_exports, {
21
21
  coerceClampedInt: () => coerceClampedInt,
22
22
  coerceFiniteNumber: () => coerceFiniteNumber,
23
23
  errText: () => errText,
24
- isTrueish: () => isTrueish
24
+ isTrueish: () => isTrueish,
25
+ oneLine: () => oneLine
25
26
  });
26
27
  module.exports = __toCommonJS(coerce_exports);
27
28
  const DECIMAL_NUMBER_RE = /^-?\d+(\.\d+)?$/;
@@ -77,11 +78,15 @@ function coerceClampedInt(raw, min, max, defaultValue) {
77
78
  }
78
79
  return Math.max(min, Math.min(max, Math.floor(n)));
79
80
  }
81
+ function oneLine(value) {
82
+ return value.replace(/[\r\n\t]+/g, " ");
83
+ }
80
84
  // Annotate the CommonJS export names for ESM import in node:
81
85
  0 && (module.exports = {
82
86
  coerceClampedInt,
83
87
  coerceFiniteNumber,
84
88
  errText,
85
- isTrueish
89
+ isTrueish,
90
+ oneLine
86
91
  });
87
92
  //# sourceMappingURL=coerce.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/coerce.ts"],
4
- "sourcesContent": ["/**\n * Boundary coercion helpers for external API data.\n *\n * The parcel.app API is documented but field types still drift in practice\n * (rare success-flag returned as `\"true\"` string, occasional null where a\n * number is expected). These helpers guard against NaN/Infinity/non-string\n * values reaching ioBroker states.\n */\n\n// Strict decimal regex \u2014 only optional minus sign + digits + optional fractional part.\n// Rejects HEX (`0x...`), exponential (`1e3`), Infinity, NaN, leading/trailing whitespace.\n// Hassemu (E8 in v1.9.0) hardened the same coerce-helper this way; homewizard\n// adopted it in v0.7.2 (D8). Consistent with both adapters.\nconst DECIMAL_NUMBER_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Coerce to a finite number or null.\n * Accepts numbers directly; parses strict decimal strings; rejects NaN, Infinity,\n * HEX (`0x...`) and exponential notation (`1e3`).\n *\n * @param value Unknown external value\n */\nexport function coerceFiniteNumber(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n if (typeof value === \"string\" && DECIMAL_NUMBER_RE.test(value)) {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/**\n * Coerce a parcel.app `success` flag. The API returns a real boolean in normal\n * operation, but the guard accepts common string/number encodings (`1`, `\"true\"`,\n * `\"1\"`) so a one-off drift doesn't break the entire poll cycle.\n *\n * @param v Value to interpret as a success flag\n */\nexport function isTrueish(v: unknown): boolean {\n if (typeof v === \"boolean\") {\n return v;\n }\n if (typeof v === \"number\") {\n return v === 1;\n }\n if (typeof v === \"string\") {\n const s = v.toLowerCase();\n return s === \"true\" || s === \"1\";\n }\n return false;\n}\n\n/**\n * Extract a log-friendly message from a thrown / rejected value. Centralizes the\n * `err instanceof Error ? err.message : String(err)` pattern that otherwise\n * gets repeated at every catch-site. Plain objects are JSON-stringified so a\n * `[object Object]` log is avoided when callers throw bag-of-fields.\n *\n * @param err Caught value of unknown shape (Error, string, undefined, ...).\n */\nexport function errText(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n if (err === null) {\n return \"null\";\n }\n if (err === undefined) {\n return \"undefined\";\n }\n if (typeof err === \"string\") {\n return err;\n }\n if (typeof err === \"number\" || typeof err === \"boolean\" || typeof err === \"bigint\") {\n return String(err);\n }\n // Plain objects + symbols would otherwise stringify to \"[object Object]\" / fail.\n // Prefer JSON for the common case so the log is at least diagnosable.\n try {\n return JSON.stringify(err);\n } catch {\n return Object.prototype.toString.call(err);\n }\n}\n\n/**\n * v0.4.2 (X5): coerce an admin-config integer setting (number-or-string)\n * to a finite, clamped integer. Returns `defaultValue` for non-finite\n * input \u2014 guards against `setInterval(fn, NaN)` tight-loops when the\n * config field happens to come back as a string from the admin UI.\n *\n * @param raw Raw value from `this.config.<field>`.\n * @param min Inclusive lower bound.\n * @param max Inclusive upper bound.\n * @param defaultValue Fallback when raw is missing or unparseable.\n */\nexport function coerceClampedInt(raw: unknown, min: number, max: number, defaultValue: number): number {\n const n = typeof raw === \"number\" ? raw : typeof raw === \"string\" ? parseFloat(raw) : NaN;\n if (!Number.isFinite(n)) {\n return defaultValue;\n }\n return Math.max(min, Math.min(max, Math.floor(n)));\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,MAAM,oBAAoB;AASnB,SAAS,mBAAmB,OAA+B;AAChE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,EAC1C;AACA,MAAI,OAAO,UAAU,YAAY,kBAAkB,KAAK,KAAK,GAAG;AAC9D,UAAM,IAAI,OAAO,KAAK;AACtB,WAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AASO,SAAS,UAAU,GAAqB;AAC7C,MAAI,OAAO,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,WAAO,MAAM;AAAA,EACf;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,UAAM,IAAI,EAAE,YAAY;AACxB,WAAO,MAAM,UAAU,MAAM;AAAA,EAC/B;AACA,SAAO;AACT;AAUO,SAAS,QAAQ,KAAsB;AAC5C,MAAI,eAAe,OAAO;AACxB,WAAO,IAAI;AAAA,EACb;AACA,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,aAAa,OAAO,QAAQ,UAAU;AAClF,WAAO,OAAO,GAAG;AAAA,EACnB;AAGA,MAAI;AACF,WAAO,KAAK,UAAU,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO,OAAO,UAAU,SAAS,KAAK,GAAG;AAAA,EAC3C;AACF;AAaO,SAAS,iBAAiB,KAAc,KAAa,KAAa,cAA8B;AACrG,QAAM,IAAI,OAAO,QAAQ,WAAW,MAAM,OAAO,QAAQ,WAAW,WAAW,GAAG,IAAI;AACtF,MAAI,CAAC,OAAO,SAAS,CAAC,GAAG;AACvB,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AACnD;",
4
+ "sourcesContent": ["/**\n * Boundary coercion helpers for external API data.\n *\n * The parcel.app API is documented but field types still drift in practice\n * (rare success-flag returned as `\"true\"` string, occasional null where a\n * number is expected). These helpers guard against NaN/Infinity/non-string\n * values reaching ioBroker states.\n */\n\n// Strict decimal regex \u2014 only optional minus sign + digits + optional fractional part.\n// Rejects HEX (`0x...`), exponential (`1e3`), Infinity, NaN, leading/trailing whitespace.\n// Hassemu (E8 in v1.9.0) hardened the same coerce-helper this way; homewizard\n// adopted it in v0.7.2 (D8). Consistent with both adapters.\nconst DECIMAL_NUMBER_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Coerce to a finite number or null.\n * Accepts numbers directly; parses strict decimal strings; rejects NaN, Infinity,\n * HEX (`0x...`) and exponential notation (`1e3`).\n *\n * @param value Unknown external value\n */\nexport function coerceFiniteNumber(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n if (typeof value === \"string\" && DECIMAL_NUMBER_RE.test(value)) {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/**\n * Coerce a parcel.app `success` flag. The API returns a real boolean in normal\n * operation, but the guard accepts common string/number encodings (`1`, `\"true\"`,\n * `\"1\"`) so a one-off drift doesn't break the entire poll cycle.\n *\n * @param v Value to interpret as a success flag\n */\nexport function isTrueish(v: unknown): boolean {\n if (typeof v === \"boolean\") {\n return v;\n }\n if (typeof v === \"number\") {\n return v === 1;\n }\n if (typeof v === \"string\") {\n const s = v.toLowerCase();\n return s === \"true\" || s === \"1\";\n }\n return false;\n}\n\n/**\n * Extract a log-friendly message from a thrown / rejected value. Centralizes the\n * `err instanceof Error ? err.message : String(err)` pattern that otherwise\n * gets repeated at every catch-site. Plain objects are JSON-stringified so a\n * `[object Object]` log is avoided when callers throw bag-of-fields.\n *\n * @param err Caught value of unknown shape (Error, string, undefined, ...).\n */\nexport function errText(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n if (err === null) {\n return \"null\";\n }\n if (err === undefined) {\n return \"undefined\";\n }\n if (typeof err === \"string\") {\n return err;\n }\n if (typeof err === \"number\" || typeof err === \"boolean\" || typeof err === \"bigint\") {\n return String(err);\n }\n // Plain objects + symbols would otherwise stringify to \"[object Object]\" / fail.\n // Prefer JSON for the common case so the log is at least diagnosable.\n try {\n return JSON.stringify(err);\n } catch {\n return Object.prototype.toString.call(err);\n }\n}\n\n/**\n * v0.4.2 (X5): coerce an admin-config integer setting (number-or-string)\n * to a finite, clamped integer. Returns `defaultValue` for non-finite\n * input \u2014 guards against `setInterval(fn, NaN)` tight-loops when the\n * config field happens to come back as a string from the admin UI.\n *\n * @param raw Raw value from `this.config.<field>`.\n * @param min Inclusive lower bound.\n * @param max Inclusive upper bound.\n * @param defaultValue Fallback when raw is missing or unparseable.\n */\nexport function coerceClampedInt(raw: unknown, min: number, max: number, defaultValue: number): number {\n const n = typeof raw === \"number\" ? raw : typeof raw === \"string\" ? parseFloat(raw) : NaN;\n if (!Number.isFinite(n)) {\n return defaultValue;\n }\n return Math.max(min, Math.min(max, Math.floor(n)));\n}\n\n/**\n * Collapse CR / LF / TAB runs in an untrusted string to a single space before\n * it is interpolated into a log line \u2014 prevents log-injection (a forged second\n * log line) from external values (tracking number, carrier code, raw API body,\n * reverse-DNS owner key, \u2026). Fleet convention (hassemu/hueemu v1.36.0 S4).\n *\n * @param value Untrusted string to flatten for single-line logging.\n */\nexport function oneLine(value: string): string {\n return value.replace(/[\\r\\n\\t]+/g, \" \");\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,MAAM,oBAAoB;AASnB,SAAS,mBAAmB,OAA+B;AAChE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,EAC1C;AACA,MAAI,OAAO,UAAU,YAAY,kBAAkB,KAAK,KAAK,GAAG;AAC9D,UAAM,IAAI,OAAO,KAAK;AACtB,WAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AASO,SAAS,UAAU,GAAqB;AAC7C,MAAI,OAAO,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,WAAO,MAAM;AAAA,EACf;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,UAAM,IAAI,EAAE,YAAY;AACxB,WAAO,MAAM,UAAU,MAAM;AAAA,EAC/B;AACA,SAAO;AACT;AAUO,SAAS,QAAQ,KAAsB;AAC5C,MAAI,eAAe,OAAO;AACxB,WAAO,IAAI;AAAA,EACb;AACA,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,aAAa,OAAO,QAAQ,UAAU;AAClF,WAAO,OAAO,GAAG;AAAA,EACnB;AAGA,MAAI;AACF,WAAO,KAAK,UAAU,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO,OAAO,UAAU,SAAS,KAAK,GAAG;AAAA,EAC3C;AACF;AAaO,SAAS,iBAAiB,KAAc,KAAa,KAAa,cAA8B;AACrG,QAAM,IAAI,OAAO,QAAQ,WAAW,MAAM,OAAO,QAAQ,WAAW,WAAW,GAAG,IAAI;AACtF,MAAI,CAAC,OAAO,SAAS,CAAC,GAAG;AACvB,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AACnD;AAUO,SAAS,QAAQ,OAAuB;AAC7C,SAAO,MAAM,QAAQ,cAAc,GAAG;AACxC;",
6
6
  "names": []
7
7
  }
@@ -52,7 +52,7 @@ class ParcelClient {
52
52
  * v0.7.2: in-flight fetch for the carrier list. The per-delivery updates run
53
53
  * in parallel (Promise.all) and each resolves carrier names — without this
54
54
  * mutex the first poll with N packages fired N identical concurrent fetches
55
- * of the static 447-entry file (and a persistently failing endpoint was
55
+ * of the static carrier-list file (and a persistently failing endpoint was
56
56
  * retried N times per poll). Same pattern as beszel's auth mutex (B1).
57
57
  */
58
58
  carrierFetchInFlight = null;
@@ -93,20 +93,25 @@ class ParcelClient {
93
93
  * @param filterMode Filter active or recent deliveries
94
94
  */
95
95
  async getDeliveries(filterMode = "active") {
96
- var _a, _b;
96
+ var _a, _b, _c;
97
97
  const response = await this.request("GET", `/deliveries/?filter_mode=${filterMode}`, true);
98
98
  if (!response || typeof response !== "object") {
99
99
  (_a = this.log) == null ? void 0 : _a.debug(`API drift: malformed response (got ${typeof response})`);
100
100
  throw apiError("API error: malformed response", "API_ERROR");
101
101
  }
102
102
  if (!(0, import_coerce.isTrueish)(response.success)) {
103
- const rawCode = typeof response.error_code === "string" ? response.error_code : "";
104
103
  const rawMsg = typeof response.error_message === "string" ? response.error_message : "";
105
- const code = rawCode || rawMsg || "UNKNOWN";
106
- (_b = this.log) == null ? void 0 : _b.debug(`API drift: success=false, code='${code}', msg='${rawMsg}'`);
107
- throw apiError(`API error: ${rawMsg || code}`, rawCode === "INVALID_API_KEY" ? "INVALID_API_KEY" : "API_ERROR");
104
+ (_b = this.log) == null ? void 0 : _b.debug(`API drift: success=false, msg='${rawMsg}'`);
105
+ throw apiError(`API error: ${rawMsg || "UNKNOWN"}`, "API_ERROR");
106
+ }
107
+ if (response.deliveries == null) {
108
+ return [];
108
109
  }
109
- return Array.isArray(response.deliveries) ? response.deliveries : [];
110
+ if (!Array.isArray(response.deliveries)) {
111
+ (_c = this.log) == null ? void 0 : _c.debug(`API drift: deliveries not an array (got ${typeof response.deliveries})`);
112
+ throw apiError("API error: deliveries not an array", "API_ERROR");
113
+ }
114
+ return response.deliveries;
110
115
  }
111
116
  /**
112
117
  * Add a new delivery to parcel.app.
@@ -134,7 +139,13 @@ class ParcelClient {
134
139
  try {
135
140
  const raw = await this.request("GET", "/supported_carriers.json", false);
136
141
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
137
- this.carrierCache = raw;
142
+ const clean = {};
143
+ for (const [code, name] of Object.entries(raw)) {
144
+ if (typeof name === "string") {
145
+ clean[code] = name;
146
+ }
147
+ }
148
+ this.carrierCache = clean;
138
149
  (_a = this.log) == null ? void 0 : _a.debug(`carriers: fetched ${Object.keys(this.carrierCache).length} entries`);
139
150
  return this.carrierCache;
140
151
  }
@@ -259,7 +270,9 @@ class ParcelClient {
259
270
  return;
260
271
  }
261
272
  const code = res.statusCode === 401 ? "INVALID_API_KEY" : res.statusCode === 403 ? "FORBIDDEN" : "HTTP_ERROR";
262
- (_b = this.log) == null ? void 0 : _b.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} ${code} (body=${raw.substring(0, 200)})`);
273
+ (_b = this.log) == null ? void 0 : _b.debug(
274
+ `HTTP ${method} ${path} \u2192 ${res.statusCode} ${code} (body=${(0, import_coerce.oneLine)(raw.substring(0, 200))})`
275
+ );
263
276
  reject(apiError(`HTTP ${res.statusCode}: ${res.statusMessage}`, code));
264
277
  return;
265
278
  }
@@ -268,8 +281,8 @@ class ParcelClient {
268
281
  (_c = this.log) == null ? void 0 : _c.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} (${Date.now() - startedAt}ms, ${bodyBytes}B)`);
269
282
  resolve(parsed);
270
283
  } catch {
271
- (_d = this.log) == null ? void 0 : _d.debug(`HTTP JSON parse fail ${path}: ${raw.substring(0, 200)}`);
272
- reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));
284
+ (_d = this.log) == null ? void 0 : _d.debug(`HTTP JSON parse fail ${path}: ${(0, import_coerce.oneLine)(raw.substring(0, 200))}`);
285
+ reject(new Error(`JSON parse error (${raw.length} bytes)`));
273
286
  }
274
287
  });
275
288
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/parcel-client.ts"],
4
- "sourcesContent": ["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport { isTrueish } from \"./coerce\";\nimport type { ParcelApiResponse, ParcelDelivery, AddDeliveryRequest, AddDeliveryResponse, CarrierMap } from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\nconst REQUEST_TIMEOUT = 15_000;\n\n/**\n * v0.4.3: optional logger injected by the adapter so the HTTPS client can\n * trace its own request/response lifecycle. When omitted (e.g. in tests),\n * every `this.log?.debug(...)` call is a no-op \u2014 keeps the bare-`apiKey`\n * constructor signature backward-compatible.\n */\nexport interface ParcelClientLogger {\n /** Adapter debug log. Called at most once per request/response decision. */\n debug(message: string): void;\n}\n/**\n * v0.4.2 (P9): hard cap on response body size. parcel.app deliveries lists\n * are tiny (~1 kB per package, max ~50 packages = 50 kB), so a 1 MiB cap is\n * 20\u00D7 the realistic max while still defending against a runaway response.\n */\nconst MAX_BODY_BYTES = 1 << 20; // 1 MiB\n\n/**\n * Build an `Error` carrying a `code` (and optional extra fields such as\n * `retryAfterSeconds`). Centralizes the `new Error(...) as Error & { code }`\n * + `err.code = \u2026` pattern that was repeated at every throw/reject site.\n *\n * @param message Human-readable error message.\n * @param code Machine-readable error code used by the adapter for classification.\n * @param extra Optional additional own-properties to attach to the error.\n */\nfunction apiError(message: string, code: string, extra?: Record<string, unknown>): Error & { code: string } {\n const err = new Error(message) as Error & { code: string };\n err.code = code;\n if (extra) {\n Object.assign(err, extra);\n }\n return err;\n}\n\n/** HTTP client for the parcel.app API */\nexport class ParcelClient {\n private apiKey: string;\n private carrierCache: CarrierMap | null = null;\n /**\n * v0.7.2: in-flight fetch for the carrier list. The per-delivery updates run\n * in parallel (Promise.all) and each resolves carrier names \u2014 without this\n * mutex the first poll with N packages fired N identical concurrent fetches\n * of the static 447-entry file (and a persistently failing endpoint was\n * retried N times per poll). Same pattern as beszel's auth mutex (B1).\n */\n private carrierFetchInFlight: Promise<CarrierMap> | null = null;\n /**\n * v0.4.2 (P1): per-request AbortController. `cancelAll()` aborts every\n * pending HTTPS request \u2014 called from the adapter's `onUnload` so a slow\n * parcel.app endpoint can't keep the adapter alive past js-controller's\n * 4-second kill deadline.\n */\n private readonly inflight = new Set<AbortController>();\n /** v0.4.3: optional logger for the HTTPS-layer trace. See {@link ParcelClientLogger}. */\n private readonly log?: ParcelClientLogger;\n /** API base URL. Overridable so tests can run the real `request()` against a local mock server. */\n private readonly baseUrl: string;\n\n /**\n * @param apiKey The parcel.app API key\n * @param log Optional adapter logger for HTTPS-layer trace (v0.4.3)\n * @param baseUrl API base URL \u2014 defaults to the production endpoint; overridden in tests\n */\n constructor(apiKey: string, log?: ParcelClientLogger, baseUrl: string = API_BASE) {\n this.apiKey = apiKey;\n this.log = log;\n this.baseUrl = baseUrl;\n }\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.\n */\n cancelAll(): void {\n // v0.4.3 (A12): trace the shutdown anchor so the adapter log shows\n // exactly how many HTTPS calls were aborted at unload.\n this.log?.debug(`cancelAll: aborting ${this.inflight.size} inflight requests`);\n for (const ctrl of this.inflight) {\n ctrl.abort();\n }\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * @param filterMode Filter active or recent deliveries\n */\n async getDeliveries(filterMode: \"active\" | \"recent\" = \"active\"): Promise<ParcelDelivery[]> {\n const response = await this.request<ParcelApiResponse>(\"GET\", `/deliveries/?filter_mode=${filterMode}`, true);\n\n // API-drift guard: response may be null or a non-object\n if (!response || typeof response !== \"object\") {\n // v0.4.3 (A11a): trace malformed-response drift before throwing.\n this.log?.debug(`API drift: malformed response (got ${typeof response})`);\n throw apiError(\"API error: malformed response\", \"API_ERROR\");\n }\n\n if (!isTrueish(response.success)) {\n const rawCode = typeof response.error_code === \"string\" ? response.error_code : \"\";\n const rawMsg = typeof response.error_message === \"string\" ? response.error_message : \"\";\n const code = rawCode || rawMsg || \"UNKNOWN\";\n // v0.4.3 (A11b): trace API-side error before throwing.\n this.log?.debug(`API drift: success=false, code='${code}', msg='${rawMsg}'`);\n throw apiError(`API error: ${rawMsg || code}`, rawCode === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\");\n }\n\n // API-drift guard: deliveries must be an array\n return Array.isArray(response.deliveries) ? response.deliveries : [];\n }\n\n /**\n * Add a new delivery to parcel.app.\n *\n * @param delivery The delivery to add\n */\n async addDelivery(delivery: AddDeliveryRequest): Promise<AddDeliveryResponse> {\n return this.request<AddDeliveryResponse>(\"POST\", \"/add-delivery/\", true, delivery);\n }\n\n /** Get carrier names (cached after first call; concurrent callers share one fetch) */\n async getCarrierNames(): Promise<CarrierMap> {\n if (this.carrierCache) {\n return this.carrierCache;\n }\n // v0.7.2: share one in-flight fetch between the parallel per-delivery\n // updates instead of firing N identical requests on the first poll.\n if (!this.carrierFetchInFlight) {\n this.carrierFetchInFlight = this.fetchCarrierNames().finally(() => {\n this.carrierFetchInFlight = null;\n });\n }\n return this.carrierFetchInFlight;\n }\n\n /** One actual carrier-list fetch. Failure \u2192 empty map, NOT cached (retry next poll). */\n private async fetchCarrierNames(): Promise<CarrierMap> {\n try {\n const raw = await this.request<unknown>(\"GET\", \"/supported_carriers.json\", false);\n // API-drift guard: must be a plain object (not null, array, or primitive)\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n this.carrierCache = raw as CarrierMap;\n // v0.4.3 (D1): trace the one-time cache fill so a successful warm-up\n // is visible in the debug log (happens once per adapter restart).\n this.log?.debug(`carriers: fetched ${Object.keys(this.carrierCache).length} entries`);\n return this.carrierCache;\n }\n // v0.4.3 (D3): non-object drift \u2014 supported_carriers.json returned\n // something that isn't an object. Empty map is returned, NOT cached.\n this.log?.debug(\n `carriers: drift (got ${Array.isArray(raw) ? \"array\" : typeof raw}, expected object), kept empty`,\n );\n return {};\n } catch (err) {\n // v0.4.3 (D2): trace the fetch-fail so the empty-map fallback isn't\n // silent. NOT cached \u2014 next poll retries; the trace then shows the\n // retry, too. Without this the user sees carrier codes instead of\n // names with no log entry explaining why.\n const msg = err instanceof Error ? err.message : String(err);\n this.log?.debug(`carriers: fetch failed (kept empty, will retry): ${msg}`);\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\n }\n\n /**\n * Resolve a carrier code to a display name.\n *\n * @param carrierCode The carrier code from API\n */\n async getCarrierName(carrierCode: unknown): Promise<string> {\n // API-drift guard: non-string codes fall back to \"UNKNOWN\"\n if (typeof carrierCode !== \"string\" || carrierCode.length === 0) {\n // v0.4.3 (D4): trace non-string code drift. Helps diagnose \"all my\n // packages show UNKNOWN carrier\" reports.\n this.log?.debug(`getCarrierName: non-string code (got ${typeof carrierCode}), returning UNKNOWN`);\n return \"UNKNOWN\";\n }\n const carriers = await this.getCarrierNames();\n const mapped = carriers[carrierCode];\n return typeof mapped === \"string\" && mapped.length > 0 ? mapped : carrierCode.toUpperCase();\n }\n\n /** Test if the API key is valid */\n async testConnection(): Promise<{ success: boolean; message: string }> {\n try {\n await this.getDeliveries(\"active\");\n return { success: true, message: \"Connection successful\" };\n } catch (err) {\n const error = err as Error & { code?: string };\n if (error.code === \"INVALID_API_KEY\") {\n return { success: false, message: \"Invalid API key\" };\n }\n return { success: false, message: error.message };\n }\n }\n\n /**\n * Execute an HTTP request against the parcel.app API.\n *\n * @param method HTTP method\n * @param path API path\n * @param authenticated Whether to send the API key\n * @param body Optional request body\n */\n private request<T>(method: string, path: string, authenticated: boolean, body?: unknown): Promise<T> {\n // v0.4.3 (A0): start timestamp for elapsed-ms in the success/timeout/error\n // log lines. One LOC, no behavior change.\n const startedAt = Date.now();\n // v0.4.3 (A1): trace request entry. ~144 calls/day at the default 10-min\n // poll interval \u2014 acceptable at debug.\n this.log?.debug(`HTTP ${method} ${path}`);\n return new Promise((resolve, reject) => {\n // v0.4.2 (E3): URL-shape validation defensive \u2014 paths are hardcoded\n // upstream but a future caller could pass garbage; surface a clear\n // error class instead of a TypeError thrown sync from the executor.\n let url: URL;\n try {\n url = new URL(`${this.baseUrl}${path}`);\n } catch {\n // v0.4.3 (A10): trace invalid-URL drift before throwing.\n this.log?.debug(`HTTP invalid URL: ${this.baseUrl}${path}`);\n reject(apiError(`Invalid URL: ${this.baseUrl}${path}`, \"INVALID_URL\"));\n return;\n }\n\n const headers: Record<string, string> = {};\n if (authenticated) {\n headers[\"api-key\"] = this.apiKey;\n }\n if (body) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n const options: https.RequestOptions = {\n hostname: url.hostname,\n port: url.port || 443,\n path: url.pathname + url.search,\n method,\n headers,\n timeout: REQUEST_TIMEOUT,\n };\n\n // v0.4.2 (P1): per-request AbortController. `cancelAll()` (called\n // from `onUnload`) aborts everything pending without waiting for\n // the configured timeout.\n const ctrl = new AbortController();\n this.inflight.add(ctrl);\n const cleanup = (): void => {\n this.inflight.delete(ctrl);\n };\n\n // Pick transport from the URL protocol so tests can run the real\n // request() against a local http mock server; production is always https.\n const transportRequest: (\n opts: https.RequestOptions,\n callback: (res: http.IncomingMessage) => void,\n ) => http.ClientRequest = url.protocol === \"http:\" ? http.request : https.request;\n\n const req = transportRequest(options, res => {\n const chunks: Buffer[] = [];\n let bodyBytes = 0;\n let oversized = false;\n\n res.on(\"error\", err => {\n cleanup();\n reject(err);\n });\n res.on(\"data\", (chunk: Buffer) => {\n if (oversized) {\n return;\n }\n bodyBytes += chunk.length;\n // v0.4.2 (P9): drop oversized responses so a compromised or\n // misconfigured endpoint can't OOM the adapter. Reject with the\n // stable BODY_TOO_LARGE code here, then destroy WITHOUT an error so\n // req.on(\"error\") doesn't fire a second, codeless rejection (the\n // earlier `req.destroy(Error)` preempted the end-handler's code).\n if (bodyBytes > MAX_BODY_BYTES) {\n oversized = true;\n // v0.4.3 (A9): trace the oversize-drop before destroying.\n this.log?.debug(`HTTP body oversized ${path}: dropping at ${bodyBytes}B`);\n cleanup();\n reject(apiError(\"Response body too large\", \"BODY_TOO_LARGE\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n res.on(\"end\", () => {\n if (oversized) {\n return; // already cleaned up + rejected in the data handler\n }\n cleanup();\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n\n if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {\n if (res.statusCode === 429) {\n // v0.4.2 (P6): clamp Retry-After parser. Bogus values (0,\n // negative, NaN) used to fall through `>0` and default to\n // 5 min \u2014 keep that, but also reject infinity/extreme.\n const retryAfter = parseInt(res.headers[\"retry-after\"] || \"\", 10);\n const retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;\n // v0.4.3 (A4): trace 429 with the parsed retry-after.\n this.log?.debug(`HTTP 429 ${path} \u2192 retry-after=${retryAfterSeconds}s`);\n reject(apiError(\"Rate limit exceeded\", \"RATE_LIMITED\", { retryAfterSeconds }));\n return;\n }\n // v0.4.2 (P3): split 401 (invalid key) from 403 (permission /\n // no premium). Adapter treats them differently \u2014 INVALID_API_KEY\n // says \"fix the key\", FORBIDDEN says \"fix the account\".\n const code =\n res.statusCode === 401 ? \"INVALID_API_KEY\" : res.statusCode === 403 ? \"FORBIDDEN\" : \"HTTP_ERROR\";\n // v0.4.3 (A3): trace 4xx/5xx with body-snippet for diagnosis.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} ${code} (body=${raw.substring(0, 200)})`);\n reject(apiError(`HTTP ${res.statusCode}: ${res.statusMessage}`, code));\n return;\n }\n\n try {\n const parsed = JSON.parse(raw) as T;\n // v0.4.3 (A2): trace successful response with elapsed-ms + bytes.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} (${Date.now() - startedAt}ms, ${bodyBytes}B)`);\n resolve(parsed);\n } catch {\n // v0.4.3 (A8): trace JSON parse-fail with snippet.\n this.log?.debug(`HTTP JSON parse fail ${path}: ${raw.substring(0, 200)}`);\n reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));\n }\n });\n });\n\n ctrl.signal.addEventListener(\"abort\", () => {\n // v0.4.3: A6 deliberately omitted \u2014 `req.destroy(Error)` propagates\n // through `req.on(\"error\")` below where A7 already logs it.\n req.destroy(new Error(\"Request aborted\"));\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n cleanup();\n // v0.4.3 (A5): trace timeout with elapsed-ms.\n this.log?.debug(`HTTP timeout ${method} ${path} (${Date.now() - startedAt}ms)`);\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", err => {\n cleanup();\n // v0.4.3 (A7): trace network / abort / TLS / DNS errors with elapsed.\n // Also catches the abort case (req.destroy(Error(\"Request aborted\")))\n // \u2014 A6 deliberately not emitted to avoid double-log.\n this.log?.debug(`HTTP error ${method} ${path} (${Date.now() - startedAt}ms): ${err.message}`);\n reject(err);\n });\n\n if (body) {\n req.write(JSON.stringify(body));\n }\n req.end();\n });\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAAsB;AACtB,YAAuB;AACvB,oBAA0B;AAG1B,MAAM,WAAW;AACjB,MAAM,kBAAkB;AAiBxB,MAAM,iBAAiB,KAAK;AAW5B,SAAS,SAAS,SAAiB,MAAc,OAA2D;AAC1G,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,MAAI,OAAO;AACX,MAAI,OAAO;AACT,WAAO,OAAO,KAAK,KAAK;AAAA,EAC1B;AACA,SAAO;AACT;AAGO,MAAM,aAAa;AAAA,EAChB;AAAA,EACA,eAAkC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQlC,uBAAmD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO1C,WAAW,oBAAI,IAAqB;AAAA;AAAA,EAEpC;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,YAAY,QAAgB,KAA0B,UAAkB,UAAU;AAChF,SAAK,SAAS;AACd,SAAK,MAAM;AACX,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,YAAkB;AAjFpB;AAoFI,eAAK,QAAL,mBAAU,MAAM,uBAAuB,KAAK,SAAS,IAAI;AACzD,eAAW,QAAQ,KAAK,UAAU;AAChC,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,cAAc,aAAkC,UAAqC;AA/F7F;AAgGI,UAAM,WAAW,MAAM,KAAK,QAA2B,OAAO,4BAA4B,UAAU,IAAI,IAAI;AAG5G,QAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAE7C,iBAAK,QAAL,mBAAU,MAAM,sCAAsC,OAAO,QAAQ;AACrE,YAAM,SAAS,iCAAiC,WAAW;AAAA,IAC7D;AAEA,QAAI,KAAC,yBAAU,SAAS,OAAO,GAAG;AAChC,YAAM,UAAU,OAAO,SAAS,eAAe,WAAW,SAAS,aAAa;AAChF,YAAM,SAAS,OAAO,SAAS,kBAAkB,WAAW,SAAS,gBAAgB;AACrF,YAAM,OAAO,WAAW,UAAU;AAElC,iBAAK,QAAL,mBAAU,MAAM,mCAAmC,IAAI,WAAW,MAAM;AACxE,YAAM,SAAS,cAAc,UAAU,IAAI,IAAI,YAAY,oBAAoB,oBAAoB,WAAW;AAAA,IAChH;AAGA,WAAO,MAAM,QAAQ,SAAS,UAAU,IAAI,SAAS,aAAa,CAAC;AAAA,EACrE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,YAAY,UAA4D;AAC5E,WAAO,KAAK,QAA6B,QAAQ,kBAAkB,MAAM,QAAQ;AAAA,EACnF;AAAA;AAAA,EAGA,MAAM,kBAAuC;AAC3C,QAAI,KAAK,cAAc;AACrB,aAAO,KAAK;AAAA,IACd;AAGA,QAAI,CAAC,KAAK,sBAAsB;AAC9B,WAAK,uBAAuB,KAAK,kBAAkB,EAAE,QAAQ,MAAM;AACjE,aAAK,uBAAuB;AAAA,MAC9B,CAAC;AAAA,IACH;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAc,oBAAyC;AA/IzD;AAgJI,QAAI;AACF,YAAM,MAAM,MAAM,KAAK,QAAiB,OAAO,4BAA4B,KAAK;AAEhF,UAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,GAAG,GAAG;AACzD,aAAK,eAAe;AAGpB,mBAAK,QAAL,mBAAU,MAAM,qBAAqB,OAAO,KAAK,KAAK,YAAY,EAAE,MAAM;AAC1E,eAAO,KAAK;AAAA,MACd;AAGA,iBAAK,QAAL,mBAAU;AAAA,QACR,wBAAwB,MAAM,QAAQ,GAAG,IAAI,UAAU,OAAO,GAAG;AAAA;AAEnE,aAAO,CAAC;AAAA,IACV,SAAS,KAAK;AAKZ,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,iBAAK,QAAL,mBAAU,MAAM,oDAAoD,GAAG;AAEvE,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,eAAe,aAAuC;AAjL9D;AAmLI,QAAI,OAAO,gBAAgB,YAAY,YAAY,WAAW,GAAG;AAG/D,iBAAK,QAAL,mBAAU,MAAM,wCAAwC,OAAO,WAAW;AAC1E,aAAO;AAAA,IACT;AACA,UAAM,WAAW,MAAM,KAAK,gBAAgB;AAC5C,UAAM,SAAS,SAAS,WAAW;AACnC,WAAO,OAAO,WAAW,YAAY,OAAO,SAAS,IAAI,SAAS,YAAY,YAAY;AAAA,EAC5F;AAAA;AAAA,EAGA,MAAM,iBAAiE;AACrE,QAAI;AACF,YAAM,KAAK,cAAc,QAAQ;AACjC,aAAO,EAAE,SAAS,MAAM,SAAS,wBAAwB;AAAA,IAC3D,SAAS,KAAK;AACZ,YAAM,QAAQ;AACd,UAAI,MAAM,SAAS,mBAAmB;AACpC,eAAO,EAAE,SAAS,OAAO,SAAS,kBAAkB;AAAA,MACtD;AACA,aAAO,EAAE,SAAS,OAAO,SAAS,MAAM,QAAQ;AAAA,IAClD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,QAAW,QAAgB,MAAc,eAAwB,MAA4B;AApNvG;AAuNI,UAAM,YAAY,KAAK,IAAI;AAG3B,eAAK,QAAL,mBAAU,MAAM,QAAQ,MAAM,IAAI,IAAI;AACtC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AA3N5C,UAAAA;AA+NM,UAAI;AACJ,UAAI;AACF,cAAM,IAAI,IAAI,GAAG,KAAK,OAAO,GAAG,IAAI,EAAE;AAAA,MACxC,QAAQ;AAEN,SAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,qBAAqB,KAAK,OAAO,GAAG,IAAI;AACxD,eAAO,SAAS,gBAAgB,KAAK,OAAO,GAAG,IAAI,IAAI,aAAa,CAAC;AACrE;AAAA,MACF;AAEA,YAAM,UAAkC,CAAC;AACzC,UAAI,eAAe;AACjB,gBAAQ,SAAS,IAAI,KAAK;AAAA,MAC5B;AACA,UAAI,MAAM;AACR,gBAAQ,cAAc,IAAI;AAAA,MAC5B;AAEA,YAAM,UAAgC;AAAA,QACpC,UAAU,IAAI;AAAA,QACd,MAAM,IAAI,QAAQ;AAAA,QAClB,MAAM,IAAI,WAAW,IAAI;AAAA,QACzB;AAAA,QACA;AAAA,QACA,SAAS;AAAA,MACX;AAKA,YAAM,OAAO,IAAI,gBAAgB;AACjC,WAAK,SAAS,IAAI,IAAI;AACtB,YAAM,UAAU,MAAY;AAC1B,aAAK,SAAS,OAAO,IAAI;AAAA,MAC3B;AAIA,YAAM,mBAGoB,IAAI,aAAa,UAAU,KAAK,UAAU,MAAM;AAE1E,YAAM,MAAM,iBAAiB,SAAS,SAAO;AAC3C,cAAM,SAAmB,CAAC;AAC1B,YAAI,YAAY;AAChB,YAAI,YAAY;AAEhB,YAAI,GAAG,SAAS,SAAO;AACrB,kBAAQ;AACR,iBAAO,GAAG;AAAA,QACZ,CAAC;AACD,YAAI,GAAG,QAAQ,CAAC,UAAkB;AAnR1C,cAAAA;AAoRU,cAAI,WAAW;AACb;AAAA,UACF;AACA,uBAAa,MAAM;AAMnB,cAAI,YAAY,gBAAgB;AAC9B,wBAAY;AAEZ,aAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,uBAAuB,IAAI,iBAAiB,SAAS;AACrE,oBAAQ;AACR,mBAAO,SAAS,2BAA2B,gBAAgB,CAAC;AAC5D,gBAAI,QAAQ;AACZ;AAAA,UACF;AACA,iBAAO,KAAK,KAAK;AAAA,QACnB,CAAC;AACD,YAAI,GAAG,OAAO,MAAM;AAxS5B,cAAAA,KAAA;AAySU,cAAI,WAAW;AACb;AAAA,UACF;AACA,kBAAQ;AACR,gBAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AAElD,cAAI,IAAI,eAAe,IAAI,aAAa,OAAO,IAAI,cAAc,MAAM;AACrE,gBAAI,IAAI,eAAe,KAAK;AAI1B,oBAAM,aAAa,SAAS,IAAI,QAAQ,aAAa,KAAK,IAAI,EAAE;AAChE,oBAAM,oBACJ,OAAO,SAAS,UAAU,KAAK,aAAa,IAAI,KAAK,IAAI,KAAK,MAAM,UAAU,IAAI,IAAI;AAExF,eAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,YAAY,IAAI,uBAAkB,iBAAiB;AACnE,qBAAO,SAAS,uBAAuB,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;AAC7E;AAAA,YACF;AAIA,kBAAM,OACJ,IAAI,eAAe,MAAM,oBAAoB,IAAI,eAAe,MAAM,cAAc;AAEtF,uBAAK,QAAL,mBAAU,MAAM,QAAQ,MAAM,IAAI,IAAI,WAAM,IAAI,UAAU,IAAI,IAAI,UAAU,IAAI,UAAU,GAAG,GAAG,CAAC;AACjG,mBAAO,SAAS,QAAQ,IAAI,UAAU,KAAK,IAAI,aAAa,IAAI,IAAI,CAAC;AACrE;AAAA,UACF;AAEA,cAAI;AACF,kBAAM,SAAS,KAAK,MAAM,GAAG;AAE7B,uBAAK,QAAL,mBAAU,MAAM,QAAQ,MAAM,IAAI,IAAI,WAAM,IAAI,UAAU,KAAK,KAAK,IAAI,IAAI,SAAS,OAAO,SAAS;AACrG,oBAAQ,MAAM;AAAA,UAChB,QAAQ;AAEN,uBAAK,QAAL,mBAAU,MAAM,wBAAwB,IAAI,KAAK,IAAI,UAAU,GAAG,GAAG,CAAC;AACtE,mBAAO,IAAI,MAAM,qBAAqB,IAAI,UAAU,GAAG,GAAG,CAAC,EAAE,CAAC;AAAA,UAChE;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAED,WAAK,OAAO,iBAAiB,SAAS,MAAM;AAG1C,YAAI,QAAQ,IAAI,MAAM,iBAAiB,CAAC;AAAA,MAC1C,CAAC;AAED,UAAI,GAAG,WAAW,MAAM;AA1V9B,YAAAA;AA2VQ,YAAI,QAAQ;AACZ,gBAAQ;AAER,SAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,gBAAgB,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS;AACzE,eAAO,IAAI,MAAM,iBAAiB,CAAC;AAAA,MACrC,CAAC;AAED,UAAI,GAAG,SAAS,SAAO;AAlW7B,YAAAA;AAmWQ,gBAAQ;AAIR,SAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,cAAc,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS,QAAQ,IAAI,OAAO;AAC1F,eAAO,GAAG;AAAA,MACZ,CAAC;AAED,UAAI,MAAM;AACR,YAAI,MAAM,KAAK,UAAU,IAAI,CAAC;AAAA,MAChC;AACA,UAAI,IAAI;AAAA,IACV,CAAC;AAAA,EACH;AACF;",
4
+ "sourcesContent": ["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport { isTrueish, oneLine } from \"./coerce\";\nimport type { ParcelApiResponse, ParcelDelivery, AddDeliveryRequest, AddDeliveryResponse, CarrierMap } from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\nconst REQUEST_TIMEOUT = 15_000;\n\n/**\n * v0.4.3: optional logger injected by the adapter so the HTTPS client can\n * trace its own request/response lifecycle. When omitted (e.g. in tests),\n * every `this.log?.debug(...)` call is a no-op \u2014 keeps the bare-`apiKey`\n * constructor signature backward-compatible.\n */\nexport interface ParcelClientLogger {\n /** Adapter debug log. Called per request/response outcome (drift, status, parse, oversize) \u2014 low-frequency tracing. */\n debug(message: string): void;\n}\n/**\n * v0.4.2 (P9): hard cap on response body size. parcel.app deliveries lists\n * are tiny (~1 kB per package, max ~50 packages = 50 kB), so a 1 MiB cap is\n * 20\u00D7 the realistic max while still defending against a runaway response.\n */\nconst MAX_BODY_BYTES = 1 << 20; // 1 MiB\n\n/**\n * Build an `Error` carrying a `code` (and optional extra fields such as\n * `retryAfterSeconds`). Centralizes the `new Error(...) as Error & { code }`\n * + `err.code = \u2026` pattern that was repeated at every throw/reject site.\n *\n * @param message Human-readable error message.\n * @param code Machine-readable error code used by the adapter for classification.\n * @param extra Optional additional own-properties to attach to the error.\n */\nfunction apiError(message: string, code: string, extra?: Record<string, unknown>): Error & { code: string } {\n const err = new Error(message) as Error & { code: string };\n err.code = code;\n if (extra) {\n Object.assign(err, extra);\n }\n return err;\n}\n\n/** HTTP client for the parcel.app API */\nexport class ParcelClient {\n private apiKey: string;\n private carrierCache: CarrierMap | null = null;\n /**\n * v0.7.2: in-flight fetch for the carrier list. The per-delivery updates run\n * in parallel (Promise.all) and each resolves carrier names \u2014 without this\n * mutex the first poll with N packages fired N identical concurrent fetches\n * of the static carrier-list file (and a persistently failing endpoint was\n * retried N times per poll). Same pattern as beszel's auth mutex (B1).\n */\n private carrierFetchInFlight: Promise<CarrierMap> | null = null;\n /**\n * v0.4.2 (P1): per-request AbortController. `cancelAll()` aborts every\n * pending HTTPS request \u2014 called from the adapter's `onUnload` so a slow\n * parcel.app endpoint can't keep the adapter alive past js-controller's\n * 4-second kill deadline.\n */\n private readonly inflight = new Set<AbortController>();\n /** v0.4.3: optional logger for the HTTPS-layer trace. See {@link ParcelClientLogger}. */\n private readonly log?: ParcelClientLogger;\n /** API base URL. Overridable so tests can run the real `request()` against a local mock server. */\n private readonly baseUrl: string;\n\n /**\n * @param apiKey The parcel.app API key\n * @param log Optional adapter logger for HTTPS-layer trace (v0.4.3)\n * @param baseUrl API base URL \u2014 defaults to the production endpoint; overridden in tests\n */\n constructor(apiKey: string, log?: ParcelClientLogger, baseUrl: string = API_BASE) {\n this.apiKey = apiKey;\n this.log = log;\n this.baseUrl = baseUrl;\n }\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.\n */\n cancelAll(): void {\n // v0.4.3 (A12): trace the shutdown anchor so the adapter log shows\n // exactly how many HTTPS calls were aborted at unload.\n this.log?.debug(`cancelAll: aborting ${this.inflight.size} inflight requests`);\n for (const ctrl of this.inflight) {\n ctrl.abort();\n }\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * @param filterMode Filter active or recent deliveries\n */\n async getDeliveries(filterMode: \"active\" | \"recent\" = \"active\"): Promise<ParcelDelivery[]> {\n const response = await this.request<ParcelApiResponse>(\"GET\", `/deliveries/?filter_mode=${filterMode}`, true);\n\n // API-drift guard: response may be null or a non-object\n if (!response || typeof response !== \"object\") {\n // v0.4.3 (A11a): trace malformed-response drift before throwing.\n this.log?.debug(`API drift: malformed response (got ${typeof response})`);\n throw apiError(\"API error: malformed response\", \"API_ERROR\");\n }\n\n if (!isTrueish(response.success)) {\n const rawMsg = typeof response.error_message === \"string\" ? response.error_message : \"\";\n // v0.4.3 (A11b): trace API-side error before throwing. An invalid key is\n // reported via HTTP 401 (handled in request()), not via a body field \u2014\n // so a `success:false` body is always a generic API_ERROR.\n this.log?.debug(`API drift: success=false, msg='${rawMsg}'`);\n throw apiError(`API error: ${rawMsg || \"UNKNOWN\"}`, \"API_ERROR\");\n }\n\n // API-drift guard. An absent OR null `deliveries` is the API's \"no\n // deliveries\" shape \u2192 [] (zero active packages is the common state; a false\n // throw there would flip the adapter to disconnected on every poll). Only a\n // PRESENT, NON-NULL, wrong-typed value (string/number/object/boolean) is\n // real drift \u2014 throw so the poll keeps the existing states stale instead of\n // reading garbage as \"zero deliveries\" and deleting every package's states.\n if (response.deliveries == null) {\n return [];\n }\n if (!Array.isArray(response.deliveries)) {\n this.log?.debug(`API drift: deliveries not an array (got ${typeof response.deliveries})`);\n throw apiError(\"API error: deliveries not an array\", \"API_ERROR\");\n }\n return response.deliveries;\n }\n\n /**\n * Add a new delivery to parcel.app.\n *\n * @param delivery The delivery to add\n */\n async addDelivery(delivery: AddDeliveryRequest): Promise<AddDeliveryResponse> {\n return this.request<AddDeliveryResponse>(\"POST\", \"/add-delivery/\", true, delivery);\n }\n\n /** Get carrier names (cached after first call; concurrent callers share one fetch) */\n async getCarrierNames(): Promise<CarrierMap> {\n if (this.carrierCache) {\n return this.carrierCache;\n }\n // v0.7.2: share one in-flight fetch between the parallel per-delivery\n // updates instead of firing N identical requests on the first poll.\n if (!this.carrierFetchInFlight) {\n this.carrierFetchInFlight = this.fetchCarrierNames().finally(() => {\n this.carrierFetchInFlight = null;\n });\n }\n return this.carrierFetchInFlight;\n }\n\n /** One actual carrier-list fetch. Failure \u2192 empty map, NOT cached (retry next poll). */\n private async fetchCarrierNames(): Promise<CarrierMap> {\n try {\n const raw = await this.request<unknown>(\"GET\", \"/supported_carriers.json\", false);\n // API-drift guard: must be a plain object (not null, array, or primitive)\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n // v0.9.0 (C6): keep only string-valued entries instead of asserting the\n // whole object is Record<string,string>. A drifted non-string value is\n // dropped here, so the cache is honestly typed (no `as CarrierMap`).\n const clean: CarrierMap = {};\n for (const [code, name] of Object.entries(raw)) {\n if (typeof name === \"string\") {\n clean[code] = name;\n }\n }\n this.carrierCache = clean;\n // v0.4.3 (D1): trace the one-time cache fill so a successful warm-up\n // is visible in the debug log (happens once per adapter restart).\n this.log?.debug(`carriers: fetched ${Object.keys(this.carrierCache).length} entries`);\n return this.carrierCache;\n }\n // v0.4.3 (D3): non-object drift \u2014 supported_carriers.json returned\n // something that isn't an object. Empty map is returned, NOT cached.\n this.log?.debug(\n `carriers: drift (got ${Array.isArray(raw) ? \"array\" : typeof raw}, expected object), kept empty`,\n );\n return {};\n } catch (err) {\n // v0.4.3 (D2): trace the fetch-fail so the empty-map fallback isn't\n // silent. NOT cached \u2014 next poll retries; the trace then shows the\n // retry, too. Without this the user sees carrier codes instead of\n // names with no log entry explaining why.\n const msg = err instanceof Error ? err.message : String(err);\n this.log?.debug(`carriers: fetch failed (kept empty, will retry): ${msg}`);\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\n }\n\n /**\n * Resolve a carrier code to a display name.\n *\n * @param carrierCode The carrier code from API\n */\n async getCarrierName(carrierCode: unknown): Promise<string> {\n // API-drift guard: non-string codes fall back to \"UNKNOWN\"\n if (typeof carrierCode !== \"string\" || carrierCode.length === 0) {\n // v0.4.3 (D4): trace non-string code drift. Helps diagnose \"all my\n // packages show UNKNOWN carrier\" reports.\n this.log?.debug(`getCarrierName: non-string code (got ${typeof carrierCode}), returning UNKNOWN`);\n return \"UNKNOWN\";\n }\n const carriers = await this.getCarrierNames();\n const mapped = carriers[carrierCode];\n return typeof mapped === \"string\" && mapped.length > 0 ? mapped : carrierCode.toUpperCase();\n }\n\n /** Test if the API key is valid */\n async testConnection(): Promise<{ success: boolean; message: string }> {\n try {\n await this.getDeliveries(\"active\");\n return { success: true, message: \"Connection successful\" };\n } catch (err) {\n const error = err as Error & { code?: string };\n if (error.code === \"INVALID_API_KEY\") {\n return { success: false, message: \"Invalid API key\" };\n }\n return { success: false, message: error.message };\n }\n }\n\n /**\n * Execute an HTTP request against the parcel.app API.\n *\n * @param method HTTP method\n * @param path API path\n * @param authenticated Whether to send the API key\n * @param body Optional request body\n */\n private request<T>(method: string, path: string, authenticated: boolean, body?: unknown): Promise<T> {\n // v0.4.3 (A0): start timestamp for elapsed-ms in the success/timeout/error\n // log lines. One LOC, no behavior change.\n const startedAt = Date.now();\n // v0.4.3 (A1): trace request entry. ~144 calls/day at the default 10-min\n // poll interval \u2014 acceptable at debug.\n this.log?.debug(`HTTP ${method} ${path}`);\n return new Promise((resolve, reject) => {\n // v0.4.2 (E3): URL-shape validation defensive \u2014 paths are hardcoded\n // upstream but a future caller could pass garbage; surface a clear\n // error class instead of a TypeError thrown sync from the executor.\n let url: URL;\n try {\n url = new URL(`${this.baseUrl}${path}`);\n } catch {\n // v0.4.3 (A10): trace invalid-URL drift before throwing.\n this.log?.debug(`HTTP invalid URL: ${this.baseUrl}${path}`);\n reject(apiError(`Invalid URL: ${this.baseUrl}${path}`, \"INVALID_URL\"));\n return;\n }\n\n const headers: Record<string, string> = {};\n if (authenticated) {\n headers[\"api-key\"] = this.apiKey;\n }\n if (body) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n const options: https.RequestOptions = {\n hostname: url.hostname,\n port: url.port || 443,\n path: url.pathname + url.search,\n method,\n headers,\n timeout: REQUEST_TIMEOUT,\n };\n\n // v0.4.2 (P1): per-request AbortController. `cancelAll()` (called\n // from `onUnload`) aborts everything pending without waiting for\n // the configured timeout.\n const ctrl = new AbortController();\n this.inflight.add(ctrl);\n const cleanup = (): void => {\n this.inflight.delete(ctrl);\n };\n\n // Pick transport from the URL protocol so tests can run the real\n // request() against a local http mock server; production is always https.\n const transportRequest: (\n opts: https.RequestOptions,\n callback: (res: http.IncomingMessage) => void,\n ) => http.ClientRequest = url.protocol === \"http:\" ? http.request : https.request;\n\n const req = transportRequest(options, res => {\n const chunks: Buffer[] = [];\n let bodyBytes = 0;\n let oversized = false;\n\n res.on(\"error\", err => {\n cleanup();\n reject(err);\n });\n res.on(\"data\", (chunk: Buffer) => {\n if (oversized) {\n return;\n }\n bodyBytes += chunk.length;\n // v0.4.2 (P9): drop oversized responses so a compromised or\n // misconfigured endpoint can't OOM the adapter. Reject with the\n // stable BODY_TOO_LARGE code here, then destroy WITHOUT an error so\n // req.on(\"error\") doesn't fire a second, codeless rejection (the\n // earlier `req.destroy(Error)` preempted the end-handler's code).\n if (bodyBytes > MAX_BODY_BYTES) {\n oversized = true;\n // v0.4.3 (A9): trace the oversize-drop before destroying.\n this.log?.debug(`HTTP body oversized ${path}: dropping at ${bodyBytes}B`);\n cleanup();\n reject(apiError(\"Response body too large\", \"BODY_TOO_LARGE\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n res.on(\"end\", () => {\n if (oversized) {\n return; // already cleaned up + rejected in the data handler\n }\n cleanup();\n // The MAX_BODY_BYTES cap above bounds `chunks`, so concat stays well\n // under Buffer's max length and toString won't throw here.\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n\n if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {\n if (res.statusCode === 429) {\n // v0.4.2 (P6): clamp Retry-After parser. Bogus values (0,\n // negative, NaN) used to fall through `>0` and default to\n // 5 min \u2014 keep that, but also reject infinity/extreme.\n const retryAfter = parseInt(res.headers[\"retry-after\"] || \"\", 10);\n const retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;\n // v0.4.3 (A4): trace 429 with the parsed retry-after.\n this.log?.debug(`HTTP 429 ${path} \u2192 retry-after=${retryAfterSeconds}s`);\n reject(apiError(\"Rate limit exceeded\", \"RATE_LIMITED\", { retryAfterSeconds }));\n return;\n }\n // v0.4.2 (P3): split 401 (invalid key) from 403 (permission /\n // no premium). Adapter treats them differently \u2014 INVALID_API_KEY\n // says \"fix the key\", FORBIDDEN says \"fix the account\".\n const code =\n res.statusCode === 401 ? \"INVALID_API_KEY\" : res.statusCode === 403 ? \"FORBIDDEN\" : \"HTTP_ERROR\";\n // v0.4.3 (A3): trace 4xx/5xx with body-snippet for diagnosis.\n this.log?.debug(\n `HTTP ${method} ${path} \u2192 ${res.statusCode} ${code} (body=${oneLine(raw.substring(0, 200))})`,\n );\n reject(apiError(`HTTP ${res.statusCode}: ${res.statusMessage}`, code));\n return;\n }\n\n try {\n const parsed = JSON.parse(raw) as T;\n // v0.4.3 (A2): trace successful response with elapsed-ms + bytes.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} (${Date.now() - startedAt}ms, ${bodyBytes}B)`);\n resolve(parsed);\n } catch {\n // v0.4.3 (A8): trace JSON parse-fail with snippet (debug only).\n this.log?.debug(`HTTP JSON parse fail ${path}: ${oneLine(raw.substring(0, 200))}`);\n // v0.9.0 (S1): keep the raw body OUT of the Error message \u2014 it\n // bubbles to a poll error-log; a malformed PII-bearing body must\n // not reach error level. The snippet stays in the debug line above.\n reject(new Error(`JSON parse error (${raw.length} bytes)`));\n }\n });\n });\n\n ctrl.signal.addEventListener(\"abort\", () => {\n // v0.4.3: A6 deliberately omitted \u2014 `req.destroy(Error)` propagates\n // through `req.on(\"error\")` below where A7 already logs it.\n req.destroy(new Error(\"Request aborted\"));\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n cleanup();\n // v0.4.3 (A5): trace timeout with elapsed-ms.\n this.log?.debug(`HTTP timeout ${method} ${path} (${Date.now() - startedAt}ms)`);\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", err => {\n cleanup();\n // v0.4.3 (A7): trace network / abort / TLS / DNS errors with elapsed.\n // Also catches the abort case (req.destroy(Error(\"Request aborted\")))\n // \u2014 A6 deliberately not emitted to avoid double-log.\n this.log?.debug(`HTTP error ${method} ${path} (${Date.now() - startedAt}ms): ${err.message}`);\n reject(err);\n });\n\n if (body) {\n req.write(JSON.stringify(body));\n }\n req.end();\n });\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAAsB;AACtB,YAAuB;AACvB,oBAAmC;AAGnC,MAAM,WAAW;AACjB,MAAM,kBAAkB;AAiBxB,MAAM,iBAAiB,KAAK;AAW5B,SAAS,SAAS,SAAiB,MAAc,OAA2D;AAC1G,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,MAAI,OAAO;AACX,MAAI,OAAO;AACT,WAAO,OAAO,KAAK,KAAK;AAAA,EAC1B;AACA,SAAO;AACT;AAGO,MAAM,aAAa;AAAA,EAChB;AAAA,EACA,eAAkC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQlC,uBAAmD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO1C,WAAW,oBAAI,IAAqB;AAAA;AAAA,EAEpC;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,YAAY,QAAgB,KAA0B,UAAkB,UAAU;AAChF,SAAK,SAAS;AACd,SAAK,MAAM;AACX,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,YAAkB;AAjFpB;AAoFI,eAAK,QAAL,mBAAU,MAAM,uBAAuB,KAAK,SAAS,IAAI;AACzD,eAAW,QAAQ,KAAK,UAAU;AAChC,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,cAAc,aAAkC,UAAqC;AA/F7F;AAgGI,UAAM,WAAW,MAAM,KAAK,QAA2B,OAAO,4BAA4B,UAAU,IAAI,IAAI;AAG5G,QAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAE7C,iBAAK,QAAL,mBAAU,MAAM,sCAAsC,OAAO,QAAQ;AACrE,YAAM,SAAS,iCAAiC,WAAW;AAAA,IAC7D;AAEA,QAAI,KAAC,yBAAU,SAAS,OAAO,GAAG;AAChC,YAAM,SAAS,OAAO,SAAS,kBAAkB,WAAW,SAAS,gBAAgB;AAIrF,iBAAK,QAAL,mBAAU,MAAM,kCAAkC,MAAM;AACxD,YAAM,SAAS,cAAc,UAAU,SAAS,IAAI,WAAW;AAAA,IACjE;AAQA,QAAI,SAAS,cAAc,MAAM;AAC/B,aAAO,CAAC;AAAA,IACV;AACA,QAAI,CAAC,MAAM,QAAQ,SAAS,UAAU,GAAG;AACvC,iBAAK,QAAL,mBAAU,MAAM,2CAA2C,OAAO,SAAS,UAAU;AACrF,YAAM,SAAS,sCAAsC,WAAW;AAAA,IAClE;AACA,WAAO,SAAS;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,YAAY,UAA4D;AAC5E,WAAO,KAAK,QAA6B,QAAQ,kBAAkB,MAAM,QAAQ;AAAA,EACnF;AAAA;AAAA,EAGA,MAAM,kBAAuC;AAC3C,QAAI,KAAK,cAAc;AACrB,aAAO,KAAK;AAAA,IACd;AAGA,QAAI,CAAC,KAAK,sBAAsB;AAC9B,WAAK,uBAAuB,KAAK,kBAAkB,EAAE,QAAQ,MAAM;AACjE,aAAK,uBAAuB;AAAA,MAC9B,CAAC;AAAA,IACH;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAc,oBAAyC;AA3JzD;AA4JI,QAAI;AACF,YAAM,MAAM,MAAM,KAAK,QAAiB,OAAO,4BAA4B,KAAK;AAEhF,UAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,GAAG,GAAG;AAIzD,cAAM,QAAoB,CAAC;AAC3B,mBAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,cAAI,OAAO,SAAS,UAAU;AAC5B,kBAAM,IAAI,IAAI;AAAA,UAChB;AAAA,QACF;AACA,aAAK,eAAe;AAGpB,mBAAK,QAAL,mBAAU,MAAM,qBAAqB,OAAO,KAAK,KAAK,YAAY,EAAE,MAAM;AAC1E,eAAO,KAAK;AAAA,MACd;AAGA,iBAAK,QAAL,mBAAU;AAAA,QACR,wBAAwB,MAAM,QAAQ,GAAG,IAAI,UAAU,OAAO,GAAG;AAAA;AAEnE,aAAO,CAAC;AAAA,IACV,SAAS,KAAK;AAKZ,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,iBAAK,QAAL,mBAAU,MAAM,oDAAoD,GAAG;AAEvE,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,eAAe,aAAuC;AAtM9D;AAwMI,QAAI,OAAO,gBAAgB,YAAY,YAAY,WAAW,GAAG;AAG/D,iBAAK,QAAL,mBAAU,MAAM,wCAAwC,OAAO,WAAW;AAC1E,aAAO;AAAA,IACT;AACA,UAAM,WAAW,MAAM,KAAK,gBAAgB;AAC5C,UAAM,SAAS,SAAS,WAAW;AACnC,WAAO,OAAO,WAAW,YAAY,OAAO,SAAS,IAAI,SAAS,YAAY,YAAY;AAAA,EAC5F;AAAA;AAAA,EAGA,MAAM,iBAAiE;AACrE,QAAI;AACF,YAAM,KAAK,cAAc,QAAQ;AACjC,aAAO,EAAE,SAAS,MAAM,SAAS,wBAAwB;AAAA,IAC3D,SAAS,KAAK;AACZ,YAAM,QAAQ;AACd,UAAI,MAAM,SAAS,mBAAmB;AACpC,eAAO,EAAE,SAAS,OAAO,SAAS,kBAAkB;AAAA,MACtD;AACA,aAAO,EAAE,SAAS,OAAO,SAAS,MAAM,QAAQ;AAAA,IAClD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,QAAW,QAAgB,MAAc,eAAwB,MAA4B;AAzOvG;AA4OI,UAAM,YAAY,KAAK,IAAI;AAG3B,eAAK,QAAL,mBAAU,MAAM,QAAQ,MAAM,IAAI,IAAI;AACtC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAhP5C,UAAAA;AAoPM,UAAI;AACJ,UAAI;AACF,cAAM,IAAI,IAAI,GAAG,KAAK,OAAO,GAAG,IAAI,EAAE;AAAA,MACxC,QAAQ;AAEN,SAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,qBAAqB,KAAK,OAAO,GAAG,IAAI;AACxD,eAAO,SAAS,gBAAgB,KAAK,OAAO,GAAG,IAAI,IAAI,aAAa,CAAC;AACrE;AAAA,MACF;AAEA,YAAM,UAAkC,CAAC;AACzC,UAAI,eAAe;AACjB,gBAAQ,SAAS,IAAI,KAAK;AAAA,MAC5B;AACA,UAAI,MAAM;AACR,gBAAQ,cAAc,IAAI;AAAA,MAC5B;AAEA,YAAM,UAAgC;AAAA,QACpC,UAAU,IAAI;AAAA,QACd,MAAM,IAAI,QAAQ;AAAA,QAClB,MAAM,IAAI,WAAW,IAAI;AAAA,QACzB;AAAA,QACA;AAAA,QACA,SAAS;AAAA,MACX;AAKA,YAAM,OAAO,IAAI,gBAAgB;AACjC,WAAK,SAAS,IAAI,IAAI;AACtB,YAAM,UAAU,MAAY;AAC1B,aAAK,SAAS,OAAO,IAAI;AAAA,MAC3B;AAIA,YAAM,mBAGoB,IAAI,aAAa,UAAU,KAAK,UAAU,MAAM;AAE1E,YAAM,MAAM,iBAAiB,SAAS,SAAO;AAC3C,cAAM,SAAmB,CAAC;AAC1B,YAAI,YAAY;AAChB,YAAI,YAAY;AAEhB,YAAI,GAAG,SAAS,SAAO;AACrB,kBAAQ;AACR,iBAAO,GAAG;AAAA,QACZ,CAAC;AACD,YAAI,GAAG,QAAQ,CAAC,UAAkB;AAxS1C,cAAAA;AAySU,cAAI,WAAW;AACb;AAAA,UACF;AACA,uBAAa,MAAM;AAMnB,cAAI,YAAY,gBAAgB;AAC9B,wBAAY;AAEZ,aAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,uBAAuB,IAAI,iBAAiB,SAAS;AACrE,oBAAQ;AACR,mBAAO,SAAS,2BAA2B,gBAAgB,CAAC;AAC5D,gBAAI,QAAQ;AACZ;AAAA,UACF;AACA,iBAAO,KAAK,KAAK;AAAA,QACnB,CAAC;AACD,YAAI,GAAG,OAAO,MAAM;AA7T5B,cAAAA,KAAA;AA8TU,cAAI,WAAW;AACb;AAAA,UACF;AACA,kBAAQ;AAGR,gBAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AAElD,cAAI,IAAI,eAAe,IAAI,aAAa,OAAO,IAAI,cAAc,MAAM;AACrE,gBAAI,IAAI,eAAe,KAAK;AAI1B,oBAAM,aAAa,SAAS,IAAI,QAAQ,aAAa,KAAK,IAAI,EAAE;AAChE,oBAAM,oBACJ,OAAO,SAAS,UAAU,KAAK,aAAa,IAAI,KAAK,IAAI,KAAK,MAAM,UAAU,IAAI,IAAI;AAExF,eAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,YAAY,IAAI,uBAAkB,iBAAiB;AACnE,qBAAO,SAAS,uBAAuB,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;AAC7E;AAAA,YACF;AAIA,kBAAM,OACJ,IAAI,eAAe,MAAM,oBAAoB,IAAI,eAAe,MAAM,cAAc;AAEtF,uBAAK,QAAL,mBAAU;AAAA,cACR,QAAQ,MAAM,IAAI,IAAI,WAAM,IAAI,UAAU,IAAI,IAAI,cAAU,uBAAQ,IAAI,UAAU,GAAG,GAAG,CAAC,CAAC;AAAA;AAE5F,mBAAO,SAAS,QAAQ,IAAI,UAAU,KAAK,IAAI,aAAa,IAAI,IAAI,CAAC;AACrE;AAAA,UACF;AAEA,cAAI;AACF,kBAAM,SAAS,KAAK,MAAM,GAAG;AAE7B,uBAAK,QAAL,mBAAU,MAAM,QAAQ,MAAM,IAAI,IAAI,WAAM,IAAI,UAAU,KAAK,KAAK,IAAI,IAAI,SAAS,OAAO,SAAS;AACrG,oBAAQ,MAAM;AAAA,UAChB,QAAQ;AAEN,uBAAK,QAAL,mBAAU,MAAM,wBAAwB,IAAI,SAAK,uBAAQ,IAAI,UAAU,GAAG,GAAG,CAAC,CAAC;AAI/E,mBAAO,IAAI,MAAM,qBAAqB,IAAI,MAAM,SAAS,CAAC;AAAA,UAC5D;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAED,WAAK,OAAO,iBAAiB,SAAS,MAAM;AAG1C,YAAI,QAAQ,IAAI,MAAM,iBAAiB,CAAC;AAAA,MAC1C,CAAC;AAED,UAAI,GAAG,WAAW,MAAM;AAtX9B,YAAAA;AAuXQ,YAAI,QAAQ;AACZ,gBAAQ;AAER,SAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,gBAAgB,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS;AACzE,eAAO,IAAI,MAAM,iBAAiB,CAAC;AAAA,MACrC,CAAC;AAED,UAAI,GAAG,SAAS,SAAO;AA9X7B,YAAAA;AA+XQ,gBAAQ;AAIR,SAAAA,MAAA,KAAK,QAAL,gBAAAA,IAAU,MAAM,cAAc,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS,QAAQ,IAAI,OAAO;AAC1F,eAAO,GAAG;AAAA,MACZ,CAAC;AAED,UAAI,MAAM;AACR,YAAI,MAAM,KAAK,UAAU,IAAI,CAAC;AAAA,MAChC;AACA,UAAI,IAAI;AAAA,IACV,CAAC;AAAA,EACH;AACF;",
6
6
  "names": ["_a"]
7
7
  }
@@ -90,8 +90,8 @@ class StateManager {
90
90
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 50) || "unknown";
91
91
  }
92
92
  /**
93
- * Parse the status code from a delivery. API documents `status_code` as
94
- * a numeric string, but we accept numbers too and fall back to 0 for drift.
93
+ * Parse the status code from a delivery. The API sends an int; we also accept
94
+ * a numeric string and fall back to the "unknown" sentinel (-1) for drift.
95
95
  *
96
96
  * @param delivery The delivery to parse
97
97
  */
@@ -131,7 +131,7 @@ class StateManager {
131
131
  if (owner !== void 0 && owner !== rawKey) {
132
132
  const suffixed = `${id}__${StateManager.shortHash(rawKey)}`;
133
133
  this.adapter.log.debug(
134
- `packageId collision: bare='${id}' owner='${owner}' new='${rawKey}' \u2192 suffixed='${suffixed}'`
134
+ `packageId collision: bare='${id}' owner='${(0, import_coerce.oneLine)(owner)}' new='${(0, import_coerce.oneLine)(rawKey)}' \u2192 suffixed='${suffixed}'`
135
135
  );
136
136
  this.idOwner.set(suffixed, rawKey);
137
137
  return suffixed;
@@ -164,7 +164,7 @@ class StateManager {
164
164
  }
165
165
  /**
166
166
  * v0.4.2 (S3): which raw-tracking-key currently "owns" each sanitized id
167
- * within the running poll. Cleared via `resetIdOwners()` between polls so
167
+ * within the running poll. Cleared via `resetPollState()` between polls so
168
168
  * the same delivery keeps its bare id as long as it's unique.
169
169
  */
170
170
  idOwner = /* @__PURE__ */ new Map();
@@ -283,11 +283,14 @@ class StateManager {
283
283
  ]);
284
284
  }
285
285
  /**
286
- * Remove deliveries that are no longer active.
286
+ * Remove deliveries that are no longer present in the API response.
287
287
  *
288
- * @param activeIds List of currently active package IDs
288
+ * @param keepIds Package IDs the API still returns this poll (kept). Every
289
+ * currently-known delivery NOT in this set is removed. The caller passes
290
+ * ALL visible package ids, not only the ones whose state-write succeeded —
291
+ * a transient write failure must not delete a still-present package.
289
292
  */
290
- async cleanupDeliveries(activeIds) {
293
+ async cleanupDeliveries(keepIds) {
291
294
  if (this.knownDeliveryIds === null) {
292
295
  const objects = await this.adapter.getObjectViewAsync("system", "device", {
293
296
  startkey: `${this.adapter.namespace}.deliveries.`,
@@ -308,8 +311,9 @@ class StateManager {
308
311
  }
309
312
  }
310
313
  }
311
- const activeSet = new Set(activeIds);
312
- const toDelete = [...this.knownDeliveryIds].filter((pkgId) => !activeSet.has(pkgId));
314
+ const keepSet = new Set(keepIds);
315
+ const toDelete = [...this.knownDeliveryIds].filter((pkgId) => !keepSet.has(pkgId));
316
+ const toDeleteSet = new Set(toDelete);
313
317
  await Promise.all(
314
318
  toDelete.map(async (pkgId) => {
315
319
  const relativeId = `deliveries.${pkgId}`;
@@ -317,24 +321,70 @@ class StateManager {
317
321
  this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);
318
322
  this.deviceWritten.delete(pkgId);
319
323
  this.valuesSig.delete(pkgId);
320
- for (const id of [...this.createdIds]) {
321
- if (id === relativeId || id.startsWith(`${relativeId}.`)) {
322
- this.createdIds.delete(id);
323
- }
324
- }
325
324
  })
326
325
  );
327
- this.knownDeliveryIds = new Set(activeSet);
326
+ if (toDeleteSet.size > 0) {
327
+ for (const id of [...this.createdIds]) {
328
+ const pkgId = id.startsWith("deliveries.") ? id.slice("deliveries.".length).split(".")[0] : "";
329
+ if (toDeleteSet.has(pkgId)) {
330
+ this.createdIds.delete(id);
331
+ }
332
+ }
333
+ }
334
+ this.knownDeliveryIds = new Set(keepSet);
335
+ }
336
+ /**
337
+ * Parse a parcel.app expected-date string to LOCAL epoch-millis.
338
+ *
339
+ * The API delivers `date_expected`/`date_expected_end` "without specific
340
+ * timezone information"; parse with explicit local calendar components so the
341
+ * value lands on the intended local day/time (`new Date("YYYY-MM-DD")` would
342
+ * be UTC midnight). `hasTime` is false for a bare date or a midnight time
343
+ * (a day, not an hour-window). Ambiguous carrier formats (dotted, weekday
344
+ * names) are deliberately NOT guessed — they return null rather than risk a
345
+ * wrong date.
346
+ *
347
+ * @param value Raw date/time string from the API
348
+ */
349
+ static parseExpectedToMs(value) {
350
+ if (typeof value !== "string") {
351
+ return null;
352
+ }
353
+ const m = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/.exec(value.trim());
354
+ if (!m) {
355
+ return null;
356
+ }
357
+ const hasClock = m[4] !== void 0;
358
+ const year = Number(m[1]);
359
+ const month = Number(m[2]);
360
+ const day = Number(m[3]);
361
+ const hour = hasClock ? Number(m[4]) : 0;
362
+ const min = hasClock ? Number(m[5]) : 0;
363
+ const sec = m[6] !== void 0 ? Number(m[6]) : 0;
364
+ if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 || sec > 59) {
365
+ return null;
366
+ }
367
+ const date = new Date(year, month - 1, day, hour, min, sec);
368
+ if (Number.isNaN(date.getTime()) || date.getMonth() !== month - 1 || date.getDate() !== day) {
369
+ return null;
370
+ }
371
+ const hasTime = hasClock && !(hour === 0 && min === 0 && sec === 0);
372
+ return { ms: date.getTime(), hasTime };
328
373
  }
329
374
  /**
330
375
  * Resolve a delivery's expected window to epoch-millis bounds. Returns null
331
- * for non-trackable status or when there is no usable start timestamp.
332
- * `end` is null when only a single expected time is known.
376
+ * for non-trackable status or when there is no usable start time.
377
+ *
378
+ * Prefers the Unix timestamp fields; for carriers that report the window only
379
+ * as a date/time string (`date_expected`/`date_expected_end`) it falls back to
380
+ * those — but only when the string carries a real time-of-day (a bare date or
381
+ * midnight is a day, not an hour-window). Carrier-agnostic.
333
382
  *
334
383
  * @param delivery The delivery data
335
384
  * @param statusCode Pre-parsed status code
336
385
  */
337
386
  windowBoundsMs(delivery, statusCode) {
387
+ var _a, _b;
338
388
  if (!TRACKABLE_STATUSES.has(statusCode)) {
339
389
  return null;
340
390
  }
@@ -346,11 +396,16 @@ class StateManager {
346
396
  const ms = ts * 1e3;
347
397
  return Number.isNaN(new Date(ms).getTime()) ? null : ms;
348
398
  };
349
- const start = toMs(delivery.timestamp_expected);
399
+ const dateMs = (value) => {
400
+ const parsed = StateManager.parseExpectedToMs(value);
401
+ return parsed && parsed.hasTime ? parsed.ms : null;
402
+ };
403
+ const start = (_a = toMs(delivery.timestamp_expected)) != null ? _a : dateMs(delivery.date_expected);
350
404
  if (start === null) {
351
405
  return null;
352
406
  }
353
- return { start, end: toMs(delivery.timestamp_expected_end) };
407
+ const end = (_b = toMs(delivery.timestamp_expected_end)) != null ? _b : dateMs(delivery.date_expected_end);
408
+ return { start, end };
354
409
  }
355
410
  /**
356
411
  * Format epoch-millis as local HH:MM.
@@ -362,7 +417,45 @@ class StateManager {
362
417
  return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
363
418
  }
364
419
  /**
365
- * Calculate a delivery time-window string only from Unix timestamps.
420
+ * Local "MM-DD HH:MM" — used when a window spans more than one calendar day.
421
+ *
422
+ * @param ms Epoch milliseconds
423
+ */
424
+ static formatDateHHMM(ms) {
425
+ const d = new Date(ms);
426
+ const mm = (d.getMonth() + 1).toString().padStart(2, "0");
427
+ const dd = d.getDate().toString().padStart(2, "0");
428
+ return `${mm}-${dd} ${StateManager.formatHHMM(ms)}`;
429
+ }
430
+ /**
431
+ * Whether two epoch-millis fall on the same LOCAL calendar day.
432
+ *
433
+ * @param aMs First epoch milliseconds
434
+ * @param bMs Second epoch milliseconds
435
+ */
436
+ static sameLocalDay(aMs, bMs) {
437
+ const a = new Date(aMs);
438
+ const b = new Date(bMs);
439
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
440
+ }
441
+ /**
442
+ * Format a start→end window as a local string. A real end (> start) on the
443
+ * SAME day renders "HH:MM - HH:MM"; an end on a LATER day carries the date on
444
+ * both sides ("12-06 14:30 - 12-08 18:30") so a multi-day window is not shown
445
+ * as if it were same-day. No end, or an end <= start (reversed/equal), renders
446
+ * just the start.
447
+ *
448
+ * @param startMs Window start (epoch ms)
449
+ * @param endMs Window end (epoch ms) or null
450
+ */
451
+ static formatWindow(startMs, endMs) {
452
+ if (endMs === null || endMs <= startMs) {
453
+ return StateManager.formatHHMM(startMs);
454
+ }
455
+ return StateManager.sameLocalDay(startMs, endMs) ? `${StateManager.formatHHMM(startMs)} - ${StateManager.formatHHMM(endMs)}` : `${StateManager.formatDateHHMM(startMs)} - ${StateManager.formatDateHHMM(endMs)}`;
456
+ }
457
+ /**
458
+ * Calculate a delivery time-window string from the resolved expected bounds.
366
459
  *
367
460
  * @param delivery The delivery data
368
461
  * @param statusCode Pre-parsed status code
@@ -372,8 +465,7 @@ class StateManager {
372
465
  if (!bounds) {
373
466
  return "";
374
467
  }
375
- const start = StateManager.formatHHMM(bounds.start);
376
- return bounds.end !== null ? `${start} - ${StateManager.formatHHMM(bounds.end)}` : start;
468
+ return StateManager.formatWindow(bounds.start, bounds.end);
377
469
  }
378
470
  /**
379
471
  * Days from today to the expected delivery date. Returns null when the
@@ -390,9 +482,9 @@ class StateManager {
390
482
  const ts = (0, import_coerce.coerceFiniteNumber)(delivery.timestamp_expected);
391
483
  if (ts !== null && ts > 0) {
392
484
  expectedDate = new Date(ts * 1e3);
393
- } else if (typeof delivery.date_expected === "string" && delivery.date_expected.length > 0) {
394
- const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(delivery.date_expected);
395
- expectedDate = dateOnly ? new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3])) : new Date(delivery.date_expected);
485
+ } else {
486
+ const parsed = StateManager.parseExpectedToMs(delivery.date_expected);
487
+ expectedDate = parsed ? new Date(parsed.ms) : null;
396
488
  }
397
489
  if (!expectedDate || isNaN(expectedDate.getTime())) {
398
490
  return null;
@@ -483,8 +575,7 @@ class StateManager {
483
575
  var _a;
484
576
  return (_a = b.end) != null ? _a : b.start;
485
577
  }));
486
- const startStr = StateManager.formatHHMM(minStart);
487
- return maxEnd > minStart ? `${startStr} - ${StateManager.formatHHMM(maxEnd)}` : startStr;
578
+ return StateManager.formatWindow(minStart, maxEnd);
488
579
  }
489
580
  /**
490
581
  * Create/extend a read-only state and set its value. Skips the