iobroker.parcelapp 0.10.2 → 0.10.4
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 +23 -22
- package/build/lib/coerce.js +16 -1
- package/build/lib/coerce.js.map +2 -2
- package/build/lib/parcel-client.js +21 -4
- package/build/lib/parcel-client.js.map +2 -2
- package/build/main.js +43 -5
- package/build/main.js.map +2 -2
- package/io-package.json +28 -31
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -35,18 +35,18 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
|
|
|
35
35
|
|
|
36
36
|
- **Node.js >= 22**
|
|
37
37
|
- **ioBroker js-controller >= 7.2.2**
|
|
38
|
-
- **ioBroker Admin >=
|
|
38
|
+
- **ioBroker Admin >= 8.0.11**
|
|
39
39
|
- **parcel.app Premium subscription** — required for API access
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
43
|
## Configuration
|
|
44
44
|
|
|
45
|
-
| Option | Description
|
|
46
|
-
| ------------------------- |
|
|
47
|
-
| **API Key** | Your parcel.app API key (get it at [web.parcelapp.net](https://web.parcelapp.net))
|
|
45
|
+
| Option | Description | Default |
|
|
46
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
|
47
|
+
| **API Key** | Your parcel.app API key (get it at [web.parcelapp.net](https://web.parcelapp.net)) | — |
|
|
48
48
|
| **Poll Interval** | How often to fetch updates (minutes). parcel.app serves the list from a ~45–90 min server cache, so shorter intervals mostly reduce the delay until a refresh is noticed. | 10 |
|
|
49
|
-
| **Auto-remove delivered** | Remove delivered packages from states automatically. When disabled, they stay until deleted in parcel.app.
|
|
49
|
+
| **Auto-remove delivered** | Remove delivered packages from states automatically. When disabled, they stay until deleted in parcel.app. | Yes |
|
|
50
50
|
|
|
51
51
|
Status labels (`Delivered`, `In Transit`, …) and delivery estimates (`today`, `tomorrow`, `in X days`) are rendered in the ioBroker system language.
|
|
52
52
|
|
|
@@ -78,13 +78,13 @@ parcelapp.0.
|
|
|
78
78
|
|
|
79
79
|
**Status codes** (`statusCode` — the primary datapoint for automations):
|
|
80
80
|
|
|
81
|
-
| Code | Meaning
|
|
82
|
-
| ---- |
|
|
83
|
-
| 0 | Delivered
|
|
84
|
-
| 1 | Frozen
|
|
85
|
-
| 2 | In Transit
|
|
86
|
-
| 3 | Awaiting Pickup
|
|
87
|
-
| 4 | Out for Delivery | -1
|
|
81
|
+
| Code | Meaning | Code | Meaning |
|
|
82
|
+
| ---- | ---------------- | ---- | ------------------------------------------------------ |
|
|
83
|
+
| 0 | Delivered | 5 | Not Found |
|
|
84
|
+
| 1 | Frozen | 6 | Delivery Attempt Failed |
|
|
85
|
+
| 2 | In Transit | 7 | Exception |
|
|
86
|
+
| 3 | Awaiting Pickup | 8 | Info Received |
|
|
87
|
+
| 4 | Out for Delivery | -1 | Unknown (unexpected API value — package stays visible) |
|
|
88
88
|
|
|
89
89
|
---
|
|
90
90
|
|
|
@@ -140,6 +140,17 @@ sendTo("parcelapp.0", "addDelivery", {
|
|
|
140
140
|
Placeholder for the next version (at the beginning of the line):
|
|
141
141
|
### **WORK IN PROGRESS**
|
|
142
142
|
-->
|
|
143
|
+
### 0.10.4 (2026-09-02)
|
|
144
|
+
|
|
145
|
+
- Fixed: A malformed reply from parcel.app (empty body or a broken delivery entry) no longer aborts the poll with a cryptic internal message — it is reported as an API problem and retried next poll.
|
|
146
|
+
- Fixed: A brief ioBroker database hiccup while marking the connection as online was mistaken for a parcel.app failure and switched the connection indicator to red.
|
|
147
|
+
- Fixed: Scripts that call checkConnection with a non-text API key now receive the regular "API key is too short" reply instead of an internal failure.
|
|
148
|
+
- Improved: Control characters in texts coming from parcel.app (carrier names, status notes) are now stripped completely before they reach the states.
|
|
149
|
+
|
|
150
|
+
### 0.10.3 (2026-08-27) — stable
|
|
151
|
+
|
|
152
|
+
- Fixed: Stopping or restarting the adapter was cut short — the stopped instance kept claiming a live connection to parcel.app instead of showing as disconnected.
|
|
153
|
+
|
|
143
154
|
### 0.10.2 (2026-08-22)
|
|
144
155
|
|
|
145
156
|
- Changed: Internal cleanup. No user-facing changes.
|
|
@@ -159,16 +170,6 @@ sendTo("parcelapp.0", "addDelivery", {
|
|
|
159
170
|
- Changed: the fallback package name ("Package …") is localized like all other texts, and the adapter is listed under a fitting admin category (misc-data).
|
|
160
171
|
- Changed: the automatic poll after adding a delivery now respects the one-minute pacing, so bulk-adds can no longer exhaust the hourly API budget.
|
|
161
172
|
|
|
162
|
-
### 0.9.0 (2026-06-23) — stable
|
|
163
|
-
|
|
164
|
-
- 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.
|
|
165
|
-
- 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.
|
|
166
|
-
|
|
167
|
-
### 0.8.0 (2026-06-19)
|
|
168
|
-
|
|
169
|
-
- 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.
|
|
170
|
-
- When adding a delivery via script, you can now set an optional tracking language and request a push confirmation.
|
|
171
|
-
|
|
172
173
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
173
174
|
|
|
174
175
|
## Support
|
package/build/lib/coerce.js
CHANGED
|
@@ -83,7 +83,22 @@ function coerceClampedInt(raw, min, max, defaultValue) {
|
|
|
83
83
|
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
84
84
|
}
|
|
85
85
|
function oneLine(value) {
|
|
86
|
-
|
|
86
|
+
var _a;
|
|
87
|
+
let out = "";
|
|
88
|
+
let inRun = false;
|
|
89
|
+
for (const ch of value) {
|
|
90
|
+
const code = (_a = ch.codePointAt(0)) != null ? _a : 0;
|
|
91
|
+
if (code < 32 || code === 127 || code === 8232 || code === 8233) {
|
|
92
|
+
if (!inRun) {
|
|
93
|
+
out += " ";
|
|
94
|
+
inRun = true;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
out += ch;
|
|
98
|
+
inRun = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
87
102
|
}
|
|
88
103
|
// Annotate the CommonJS export names for ESM import in node:
|
|
89
104
|
0 && (module.exports = {
|
package/build/lib/coerce.js.map
CHANGED
|
@@ -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 if (typeof err === \"symbol\") {\n // JSON.stringify(Symbol()) returns undefined (it does NOT throw), so the\n // catch below would never run and the declared `string` return would be a\n // lie. String(symbol) is the only safe conversion \u2014 `${symbol}` throws.\n return String(err);\n }\n // Plain objects would otherwise stringify to \"[object Object]\". Prefer JSON so\n // the log is at least diagnosable; circular structures fall back to the tag.\n try {\n // A function, or an object whose toJSON drops everything, also yields\n // undefined here \u2014 fall back rather than returning a non-string.\n return JSON.stringify(err) ?? Object.prototype.toString.call(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 control-character runs
|
|
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;AA9D9C;AA+DE,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;AACA,MAAI,OAAO,QAAQ,UAAU;AAI3B,WAAO,OAAO,GAAG;AAAA,EACnB;AAGA,MAAI;AAGF,YAAO,UAAK,UAAU,GAAG,MAAlB,YAAuB,OAAO,UAAU,SAAS,KAAK,GAAG;AAAA,EAClE,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 if (typeof err === \"symbol\") {\n // JSON.stringify(Symbol()) returns undefined (it does NOT throw), so the\n // catch below would never run and the declared `string` return would be a\n // lie. String(symbol) is the only safe conversion \u2014 `${symbol}` throws.\n return String(err);\n }\n // Plain objects would otherwise stringify to \"[object Object]\". Prefer JSON so\n // the log is at least diagnosable; circular structures fall back to the tag.\n try {\n // A function, or an object whose toJSON drops everything, also yields\n // undefined here \u2014 fall back rather than returning a non-string.\n return JSON.stringify(err) ?? Object.prototype.toString.call(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 control-character runs in an untrusted string to a single space\n * before it is interpolated into a log line \u2014 prevents log-injection (a forged\n * second log line) and smuggled terminal escapes from external values\n * (tracking number, carrier code, raw API body, collision raw-key with its NUL\n * separator, \u2026). Covers the whole C0 range (CR/LF/TAB/NUL/VT/FF, ESC, \u2026), DEL\n * and the Unicode line separators U+2028/U+2029. Fleet convention (hassemu /\n * hueemu); widened from the line-break set in v0.10.0 (I10) to the full range\n * in 0.10.4 \u2014 as a character loop, because a regex literal with control\n * characters is rejected by the lint (no-control-regex).\n *\n * @param value Untrusted string to flatten for single-line logging.\n */\nexport function oneLine(value: string): string {\n let out = \"\";\n let inRun = false;\n for (const ch of value) {\n const code = ch.codePointAt(0) ?? 0;\n if (code < 0x20 || code === 0x7f || code === 0x2028 || code === 0x2029) {\n if (!inRun) {\n out += \" \";\n inRun = true;\n }\n } else {\n out += ch;\n inRun = false;\n }\n }\n return out;\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;AA9D9C;AA+DE,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;AACA,MAAI,OAAO,QAAQ,UAAU;AAI3B,WAAO,OAAO,GAAG;AAAA,EACnB;AAGA,MAAI;AAGF,YAAO,UAAK,UAAU,GAAG,MAAlB,YAAuB,OAAO,UAAU,SAAS,KAAK,GAAG;AAAA,EAClE,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;AAeO,SAAS,QAAQ,OAAuB;AA/H/C;AAgIE,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,aAAW,MAAM,OAAO;AACtB,UAAM,QAAO,QAAG,YAAY,CAAC,MAAhB,YAAqB;AAClC,QAAI,OAAO,MAAQ,SAAS,OAAQ,SAAS,QAAU,SAAS,MAAQ;AACtE,UAAI,CAAC,OAAO;AACV,eAAO;AACP,gBAAQ;AAAA,MACV;AAAA,IACF,OAAO;AACL,aAAO;AACP,cAAQ;AAAA,IACV;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -120,7 +120,7 @@ class ParcelClient {
|
|
|
120
120
|
* @param filterMode Filter active or recent deliveries
|
|
121
121
|
*/
|
|
122
122
|
async getDeliveries(filterMode = "active") {
|
|
123
|
-
var _a, _b, _c;
|
|
123
|
+
var _a, _b, _c, _d;
|
|
124
124
|
const response = await this.request("GET", `/deliveries/?filter_mode=${filterMode}`, true);
|
|
125
125
|
if (!response || typeof response !== "object") {
|
|
126
126
|
(_a = this.log) == null ? void 0 : _a.debug(`API drift: malformed response (got ${typeof response})`);
|
|
@@ -138,19 +138,36 @@ class ParcelClient {
|
|
|
138
138
|
(_c = this.log) == null ? void 0 : _c.debug(`API drift: deliveries not an array (got ${typeof response.deliveries})`);
|
|
139
139
|
throw apiError("API error: deliveries not an array", "API_ERROR");
|
|
140
140
|
}
|
|
141
|
+
const entries = response.deliveries;
|
|
142
|
+
const bad = entries.findIndex((d) => d === null || typeof d !== "object" || Array.isArray(d));
|
|
143
|
+
if (bad !== -1) {
|
|
144
|
+
(_d = this.log) == null ? void 0 : _d.debug(
|
|
145
|
+
`API drift: deliveries[${bad}] is not an object (got ${Array.isArray(entries[bad]) ? "array" : typeof entries[bad]})`
|
|
146
|
+
);
|
|
147
|
+
throw apiError("API error: malformed delivery entry", "API_ERROR");
|
|
148
|
+
}
|
|
141
149
|
return response.deliveries;
|
|
142
150
|
}
|
|
143
151
|
/**
|
|
144
152
|
* Add a new delivery to parcel.app.
|
|
145
153
|
*
|
|
146
154
|
* Error style: transport/HTTP failures reject with {@link ApiError}; a 2xx
|
|
147
|
-
* body is returned
|
|
148
|
-
*
|
|
155
|
+
* body is returned as-is once it is a plain object (its fields are not
|
|
156
|
+
* validated) — `success: false` is passed through unchanged because sendTo
|
|
157
|
+
* callers receive this object verbatim.
|
|
149
158
|
*
|
|
150
159
|
* @param delivery The delivery to add
|
|
151
160
|
*/
|
|
152
161
|
async addDelivery(delivery) {
|
|
153
|
-
|
|
162
|
+
var _a;
|
|
163
|
+
const response = await this.request("POST", "/add-delivery/", true, delivery);
|
|
164
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
165
|
+
(_a = this.log) == null ? void 0 : _a.debug(
|
|
166
|
+
`API drift: malformed add-delivery response (got ${Array.isArray(response) ? "array" : typeof response})`
|
|
167
|
+
);
|
|
168
|
+
throw apiError("API error: malformed response", "API_ERROR");
|
|
169
|
+
}
|
|
170
|
+
return response;
|
|
154
171
|
}
|
|
155
172
|
/** Get carrier names (cached after first call; concurrent callers share one fetch) */
|
|
156
173
|
async getCarrierNames() {
|
|
@@ -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 { errText, isTrueish, oneLine } from \"./coerce\";\nimport type {\n ApiError,\n ApiErrorCode,\n ParcelApiResponse,\n ParcelDelivery,\n AddDeliveryRequest,\n AddDeliveryResponse,\n CarrierMap,\n} from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\n/** Socket IDLE timeout \u2014 fires only when the connection goes silent. */\nconst REQUEST_TIMEOUT = 15_000;\n/**\n * Hard per-request deadline. The socket idle timeout above never fires against\n * a trickle response (a byte every few seconds), which would otherwise pin\n * `isPolling` forever and silently stop the poll loop until a restart. This\n * timer caps the TOTAL request duration regardless of socket activity.\n */\nconst REQUEST_DEADLINE_MS = 60_000;\n/** Shared Retry-After clamps (used by the client parser and the adapter cooldown). */\nexport const RETRY_AFTER_MAX_SEC = 24 * 3600;\nexport const RETRY_AFTER_DEFAULT_SEC = 5 * 60;\n/** Max chars of a response body quoted into a debug log line. */\nconst BODY_SNIPPET_LEN = 200;\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/**\n * Timeout overrides. Production never passes these \u2014 the defaults\n * ({@link REQUEST_TIMEOUT} / {@link REQUEST_DEADLINE_MS}) apply. Same seam idea\n * as the `baseUrl` parameter: without it the two watchdogs below could only be\n * exercised by a test that waits 15 respectively 60 seconds, so they stayed\n * untested \u2014 and an untested watchdog is exactly the one that silently stops\n * working (test audit 2026-08-22, finding C14).\n */\nexport interface ParcelClientTimeouts {\n /** Socket idle timeout (ms). */\n idleMs?: number;\n /** Hard per-request deadline (ms). */\n deadlineMs?: number;\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 {@link ApiError} carrying a typed `code` (and optional extra fields\n * such as `retryAfterSeconds`). Centralizes the `new Error(...)` + `err.code`\n * pattern; the `ApiErrorCode` union makes a typo on either side of the\n * client\u2194adapter contract a compile error.\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: ApiErrorCode, extra?: Record<string, unknown>): ApiError {\n const err = new Error(message) as ApiError;\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 /** Socket idle timeout in ms \u2014 same seam idea as {@link baseUrl}, see {@link ParcelClientTimeouts}. */\n private readonly idleTimeoutMs: number;\n /** Hard per-request deadline in ms \u2014 see {@link ParcelClientTimeouts}. */\n private readonly deadlineMs: number;\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 * @param timeouts Timeout overrides \u2014 production always uses the defaults\n */\n constructor(\n apiKey: string,\n log?: ParcelClientLogger,\n baseUrl: string = API_BASE,\n timeouts: ParcelClientTimeouts = {},\n ) {\n this.apiKey = apiKey;\n this.log = log;\n this.baseUrl = baseUrl;\n this.idleTimeoutMs = timeouts.idleMs ?? REQUEST_TIMEOUT;\n this.deadlineMs = timeouts.deadlineMs ?? REQUEST_DEADLINE_MS;\n }\n\n /**\n * v0.10.0 (L3): once cancelAll ran, the client is terminal \u2014 a request\n * STARTED after the abort (e.g. the carrier fetch kicked off by a poll\n * batch that was already past getDeliveries at unload) must not open a\n * fresh HTTPS connection that could outlive js-controller's 4s kill\n * deadline. `request()` rejects immediately when this is set.\n */\n private cancelled = false;\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request and refuse new ones.\n * 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 this.cancelled = true;\n for (const ctrl of this.inflight) {\n ctrl.abort();\n }\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * Error style: rejects with a code-bearing {@link ApiError} on every failure\n * (HTTP status, drift, transport) \u2014 callers classify via `error.code`.\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 // v0.10.0 (M6): the external error_message is flattened + capped before it\n // reaches any log sink \u2014 it bubbles into the poll error-log via the Error\n // message, and an unsanitized multi-line value would forge log lines.\n const rawMsg =\n typeof response.error_message === \"string\" ? oneLine(response.error_message).slice(0, BODY_SNIPPET_LEN) : \"\";\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 * Error style: transport/HTTP failures reject with {@link ApiError}; a 2xx\n * body is returned RAW and never validated \u2014 `success: false` is passed\n * through unchanged because sendTo callers receive this object verbatim.\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 /**\n * One actual carrier-list fetch. Failure \u2192 empty map, NOT cached \u2014 retried by\n * the next update batch (the mutex above only dedupes CONCURRENT callers, so\n * a poll with several 25er batches may retry once per batch; the endpoint is\n * a static, unauthenticated file without a rate limit).\n */\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 this.log?.debug(`carriers: fetch failed (kept empty, will retry): ${errText(err)}`);\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 /**\n * Test if the API key is valid.\n *\n * Error style: never throws \u2014 failures are folded into the returned\n * `{ success: false, message }` result object.\n */\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.10.0 (L3): terminal after cancelAll \u2014 a request started AFTER the\n // shutdown abort must not open a fresh connection.\n if (this.cancelled) {\n this.log?.debug(`HTTP ${method} ${path} refused \u2014 client cancelled`);\n reject(apiError(\"Client cancelled\", \"ABORTED\"));\n return;\n }\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: this.idleTimeoutMs,\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 // Marks the request as finished for the deadline listener below \u2014\n // every terminal path runs cleanup().\n let settled = false;\n const cleanup = (): void => {\n settled = true;\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 // Not covered by a test on purpose: this branch only runs for a chunk\n // that was already buffered when `req.destroy()` below fired, i.e. a\n // delivery race no test can trigger deterministically. A test that\n // hits it \"usually\" would be exactly the kind of flaky check the\n // 2026-08-22 audit removed elsewhere.\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 const httpError = ParcelClient.mapHttpStatusError(\n res.statusCode,\n res.statusMessage,\n res.headers[\"retry-after\"],\n );\n // v0.4.3 (A3/A4): trace 4xx/5xx with code, retry-after and body-snippet.\n this.log?.debug(\n `HTTP ${method} ${path} \u2192 ${res.statusCode} ${httpError.code}` +\n `${httpError.retryAfterSeconds !== undefined ? ` retry-after=${httpError.retryAfterSeconds}s` : \"\"}` +\n ` (body=${oneLine(raw.substring(0, BODY_SNIPPET_LEN))})`,\n );\n reject(httpError);\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, BODY_SNIPPET_LEN))}`);\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(apiError(`JSON parse error (${raw.length} bytes)`, \"PARSE_ERROR\"));\n }\n });\n });\n\n // v0.10.0 (M4): arm the hard deadline via AbortSignal.timeout \u2014 an\n // unref'd platform timer (never keeps the process alive, no adapter\n // context needed). Destroying with a TIMEOUT-coded ApiError routes\n // through req.on(\"error\") below, which rejects + cleans up \u2014 a trickle\n // response (a byte every few seconds) can no longer pin the poll loop.\n AbortSignal.timeout(this.deadlineMs).addEventListener(\"abort\", () => {\n if (settled) {\n return; // request finished long ago \u2014 nothing to kill, nothing to log\n }\n this.log?.debug(`HTTP deadline ${method} ${path} (${Date.now() - startedAt}ms > ${this.deadlineMs}ms)`);\n req.destroy(apiError(`Request deadline exceeded (${this.deadlineMs / 1000}s)`, \"TIMEOUT\"));\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 // v0.10.0 (M1): carries the ABORTED code so the adapter routes an\n // expected shutdown-abort to debug instead of an error log line.\n req.destroy(apiError(\"Request aborted\", \"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(apiError(\"Request timeout\", \"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(ApiError)) \u2014 A6 deliberately\n // 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 // v0.10.0 (I8): a synchronous throw from stringify/write/end (circular\n // body, stream state) must not strand the AbortController in `inflight`\n // \u2014 cancelAll's invariant is \"inflight mirrors live requests exactly\".\n try {\n if (body) {\n req.write(JSON.stringify(body));\n }\n req.end();\n } catch (err) {\n cleanup();\n req.destroy();\n reject(apiError(`Request write failed: ${errText(err)}`, \"API_ERROR\"));\n }\n });\n }\n\n /**\n * Map a non-2xx HTTP status to its {@link ApiError}. Pure \u2014 extracted from\n * the end-handler so the 401/403/429 rules read in isolation (v0.10.0, L17).\n *\n * @param statusCode HTTP status code (non-2xx)\n * @param statusMessage HTTP status message\n * @param retryAfterHeader Raw Retry-After header value (429 only)\n */\n private static mapHttpStatusError(\n statusCode: number,\n statusMessage: string | undefined,\n retryAfterHeader: string | undefined,\n ): ApiError {\n if (statusCode === 429) {\n // v0.4.2 (P6): clamp Retry-After. Bogus values (0, negative, NaN) fall\n // back to the default; extreme values are capped.\n const retryAfter = parseInt(retryAfterHeader || \"\", 10);\n const retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0\n ? Math.min(RETRY_AFTER_MAX_SEC, retryAfter)\n : RETRY_AFTER_DEFAULT_SEC;\n return apiError(\"Rate limit exceeded\", \"RATE_LIMITED\", { retryAfterSeconds });\n }\n // v0.4.2 (P3): split 401 (invalid key) from 403 (permission / no premium).\n // Adapter treats them differently \u2014 INVALID_API_KEY says \"fix the key\",\n // FORBIDDEN says \"fix the account\".\n const code: ApiErrorCode = statusCode === 401 ? \"INVALID_API_KEY\" : statusCode === 403 ? \"FORBIDDEN\" : \"HTTP_ERROR\";\n return apiError(`HTTP ${statusCode}: ${statusMessage}`, code);\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAAsB;AACtB,YAAuB;AACvB,oBAA4C;AAW5C,MAAM,WAAW;AAEjB,MAAM,kBAAkB;AAOxB,MAAM,sBAAsB;AAErB,MAAM,sBAAsB,KAAK;AACjC,MAAM,0BAA0B,IAAI;AAE3C,MAAM,mBAAmB;AAgCzB,MAAM,iBAAiB,KAAK;AAY5B,SAAS,SAAS,SAAiB,MAAoB,OAA2C;AAChG,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,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQjB,YACE,QACA,KACA,UAAkB,UAClB,WAAiC,CAAC,GAClC;AAvHJ;AAwHI,SAAK,SAAS;AACd,SAAK,MAAM;AACX,SAAK,UAAU;AACf,SAAK,iBAAgB,cAAS,WAAT,YAAmB;AACxC,SAAK,cAAa,cAAS,eAAT,YAAuB;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAMpB,YAAkB;AA5IpB;AA+II,eAAK,QAAL,mBAAU,MAAM,uBAAuB,KAAK,SAAS,IAAI;AACzD,SAAK,YAAY;AACjB,eAAW,QAAQ,KAAK,UAAU;AAChC,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,cAAc,aAAkC,UAAqC;AA9J7F;AA+JI,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;AAIhC,YAAM,SACJ,OAAO,SAAS,kBAAkB,eAAW,uBAAQ,SAAS,aAAa,EAAE,MAAM,GAAG,gBAAgB,IAAI;AAI5G,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;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport { errText, isTrueish, oneLine } from \"./coerce\";\nimport type {\n ApiError,\n ApiErrorCode,\n ParcelApiResponse,\n ParcelDelivery,\n AddDeliveryRequest,\n AddDeliveryResponse,\n CarrierMap,\n} from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\n/** Socket IDLE timeout \u2014 fires only when the connection goes silent. */\nconst REQUEST_TIMEOUT = 15_000;\n/**\n * Hard per-request deadline. The socket idle timeout above never fires against\n * a trickle response (a byte every few seconds), which would otherwise pin\n * `isPolling` forever and silently stop the poll loop until a restart. This\n * timer caps the TOTAL request duration regardless of socket activity.\n */\nconst REQUEST_DEADLINE_MS = 60_000;\n/** Shared Retry-After clamps (used by the client parser and the adapter cooldown). */\nexport const RETRY_AFTER_MAX_SEC = 24 * 3600;\nexport const RETRY_AFTER_DEFAULT_SEC = 5 * 60;\n/** Max chars of a response body quoted into a debug log line. */\nconst BODY_SNIPPET_LEN = 200;\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/**\n * Timeout overrides. Production never passes these \u2014 the defaults\n * ({@link REQUEST_TIMEOUT} / {@link REQUEST_DEADLINE_MS}) apply. Same seam idea\n * as the `baseUrl` parameter: without it the two watchdogs below could only be\n * exercised by a test that waits 15 respectively 60 seconds, so they stayed\n * untested \u2014 and an untested watchdog is exactly the one that silently stops\n * working (test audit 2026-08-22, finding C14).\n */\nexport interface ParcelClientTimeouts {\n /** Socket idle timeout (ms). */\n idleMs?: number;\n /** Hard per-request deadline (ms). */\n deadlineMs?: number;\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 {@link ApiError} carrying a typed `code` (and optional extra fields\n * such as `retryAfterSeconds`). Centralizes the `new Error(...)` + `err.code`\n * pattern; the `ApiErrorCode` union makes a typo on either side of the\n * client\u2194adapter contract a compile error.\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: ApiErrorCode, extra?: Record<string, unknown>): ApiError {\n const err = new Error(message) as ApiError;\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 /** Socket idle timeout in ms \u2014 same seam idea as {@link baseUrl}, see {@link ParcelClientTimeouts}. */\n private readonly idleTimeoutMs: number;\n /** Hard per-request deadline in ms \u2014 see {@link ParcelClientTimeouts}. */\n private readonly deadlineMs: number;\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 * @param timeouts Timeout overrides \u2014 production always uses the defaults\n */\n constructor(\n apiKey: string,\n log?: ParcelClientLogger,\n baseUrl: string = API_BASE,\n timeouts: ParcelClientTimeouts = {},\n ) {\n this.apiKey = apiKey;\n this.log = log;\n this.baseUrl = baseUrl;\n this.idleTimeoutMs = timeouts.idleMs ?? REQUEST_TIMEOUT;\n this.deadlineMs = timeouts.deadlineMs ?? REQUEST_DEADLINE_MS;\n }\n\n /**\n * v0.10.0 (L3): once cancelAll ran, the client is terminal \u2014 a request\n * STARTED after the abort (e.g. the carrier fetch kicked off by a poll\n * batch that was already past getDeliveries at unload) must not open a\n * fresh HTTPS connection that could outlive js-controller's 4s kill\n * deadline. `request()` rejects immediately when this is set.\n */\n private cancelled = false;\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request and refuse new ones.\n * 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 this.cancelled = true;\n for (const ctrl of this.inflight) {\n ctrl.abort();\n }\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * Error style: rejects with a code-bearing {@link ApiError} on every failure\n * (HTTP status, drift, transport) \u2014 callers classify via `error.code`.\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 // v0.10.0 (M6): the external error_message is flattened + capped before it\n // reaches any log sink \u2014 it bubbles into the poll error-log via the Error\n // message, and an unsanitized multi-line value would forge log lines.\n const rawMsg =\n typeof response.error_message === \"string\" ? oneLine(response.error_message).slice(0, BODY_SNIPPET_LEN) : \"\";\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 // Same drift class one level down: an entry that is not an object (null,\n // number, string, nested array) would blow up deep inside the poll \u2014 a\n // TypeError from parseStatus reading `.status_code`, classified UNKNOWN and\n // logged as \"Poll failed: Cannot read properties of null\". Fail the same way\n // the wrong-typed list does: a coded API error, existing states kept stale.\n const entries: unknown[] = response.deliveries;\n const bad = entries.findIndex(d => d === null || typeof d !== \"object\" || Array.isArray(d));\n if (bad !== -1) {\n this.log?.debug(\n `API drift: deliveries[${bad}] is not an object (got ${Array.isArray(entries[bad]) ? \"array\" : typeof entries[bad]})`,\n );\n throw apiError(\"API error: malformed delivery entry\", \"API_ERROR\");\n }\n return response.deliveries;\n }\n\n /**\n * Add a new delivery to parcel.app.\n *\n * Error style: transport/HTTP failures reject with {@link ApiError}; a 2xx\n * body is returned as-is once it is a plain object (its fields are not\n * validated) \u2014 `success: false` is passed through unchanged because sendTo\n * callers receive this object verbatim.\n *\n * @param delivery The delivery to add\n */\n async addDelivery(delivery: AddDeliveryRequest): Promise<AddDeliveryResponse> {\n const response = await this.request<unknown>(\"POST\", \"/add-delivery/\", true, delivery);\n // API-drift guard: the body goes to the sendTo caller verbatim and the\n // adapter reads `.success` from it \u2014 a null/array/primitive body used to\n // surface as a TypeError (\"Cannot read properties of null\") instead of a\n // clear API error.\n if (!response || typeof response !== \"object\" || Array.isArray(response)) {\n this.log?.debug(\n `API drift: malformed add-delivery response (got ${Array.isArray(response) ? \"array\" : typeof response})`,\n );\n throw apiError(\"API error: malformed response\", \"API_ERROR\");\n }\n return response as AddDeliveryResponse;\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 /**\n * One actual carrier-list fetch. Failure \u2192 empty map, NOT cached \u2014 retried by\n * the next update batch (the mutex above only dedupes CONCURRENT callers, so\n * a poll with several 25er batches may retry once per batch; the endpoint is\n * a static, unauthenticated file without a rate limit).\n */\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 this.log?.debug(`carriers: fetch failed (kept empty, will retry): ${errText(err)}`);\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 /**\n * Test if the API key is valid.\n *\n * Error style: never throws \u2014 failures are folded into the returned\n * `{ success: false, message }` result object.\n */\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.10.0 (L3): terminal after cancelAll \u2014 a request started AFTER the\n // shutdown abort must not open a fresh connection.\n if (this.cancelled) {\n this.log?.debug(`HTTP ${method} ${path} refused \u2014 client cancelled`);\n reject(apiError(\"Client cancelled\", \"ABORTED\"));\n return;\n }\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: this.idleTimeoutMs,\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 // Marks the request as finished for the deadline listener below \u2014\n // every terminal path runs cleanup().\n let settled = false;\n const cleanup = (): void => {\n settled = true;\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 // Not covered by a test on purpose: this branch only runs for a chunk\n // that was already buffered when `req.destroy()` below fired, i.e. a\n // delivery race no test can trigger deterministically. A test that\n // hits it \"usually\" would be exactly the kind of flaky check the\n // 2026-08-22 audit removed elsewhere.\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 const httpError = ParcelClient.mapHttpStatusError(\n res.statusCode,\n res.statusMessage,\n res.headers[\"retry-after\"],\n );\n // v0.4.3 (A3/A4): trace 4xx/5xx with code, retry-after and body-snippet.\n this.log?.debug(\n `HTTP ${method} ${path} \u2192 ${res.statusCode} ${httpError.code}` +\n `${httpError.retryAfterSeconds !== undefined ? ` retry-after=${httpError.retryAfterSeconds}s` : \"\"}` +\n ` (body=${oneLine(raw.substring(0, BODY_SNIPPET_LEN))})`,\n );\n reject(httpError);\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, BODY_SNIPPET_LEN))}`);\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(apiError(`JSON parse error (${raw.length} bytes)`, \"PARSE_ERROR\"));\n }\n });\n });\n\n // v0.10.0 (M4): arm the hard deadline via AbortSignal.timeout \u2014 an\n // unref'd platform timer (never keeps the process alive, no adapter\n // context needed). Destroying with a TIMEOUT-coded ApiError routes\n // through req.on(\"error\") below, which rejects + cleans up \u2014 a trickle\n // response (a byte every few seconds) can no longer pin the poll loop.\n AbortSignal.timeout(this.deadlineMs).addEventListener(\"abort\", () => {\n if (settled) {\n return; // request finished long ago \u2014 nothing to kill, nothing to log\n }\n this.log?.debug(`HTTP deadline ${method} ${path} (${Date.now() - startedAt}ms > ${this.deadlineMs}ms)`);\n req.destroy(apiError(`Request deadline exceeded (${this.deadlineMs / 1000}s)`, \"TIMEOUT\"));\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 // v0.10.0 (M1): carries the ABORTED code so the adapter routes an\n // expected shutdown-abort to debug instead of an error log line.\n req.destroy(apiError(\"Request aborted\", \"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(apiError(\"Request timeout\", \"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(ApiError)) \u2014 A6 deliberately\n // 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 // v0.10.0 (I8): a synchronous throw from stringify/write/end (circular\n // body, stream state) must not strand the AbortController in `inflight`\n // \u2014 cancelAll's invariant is \"inflight mirrors live requests exactly\".\n try {\n if (body) {\n req.write(JSON.stringify(body));\n }\n req.end();\n } catch (err) {\n cleanup();\n req.destroy();\n reject(apiError(`Request write failed: ${errText(err)}`, \"API_ERROR\"));\n }\n });\n }\n\n /**\n * Map a non-2xx HTTP status to its {@link ApiError}. Pure \u2014 extracted from\n * the end-handler so the 401/403/429 rules read in isolation (v0.10.0, L17).\n *\n * @param statusCode HTTP status code (non-2xx)\n * @param statusMessage HTTP status message\n * @param retryAfterHeader Raw Retry-After header value (429 only)\n */\n private static mapHttpStatusError(\n statusCode: number,\n statusMessage: string | undefined,\n retryAfterHeader: string | undefined,\n ): ApiError {\n if (statusCode === 429) {\n // v0.4.2 (P6): clamp Retry-After. Bogus values (0, negative, NaN) fall\n // back to the default; extreme values are capped.\n const retryAfter = parseInt(retryAfterHeader || \"\", 10);\n const retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0\n ? Math.min(RETRY_AFTER_MAX_SEC, retryAfter)\n : RETRY_AFTER_DEFAULT_SEC;\n return apiError(\"Rate limit exceeded\", \"RATE_LIMITED\", { retryAfterSeconds });\n }\n // v0.4.2 (P3): split 401 (invalid key) from 403 (permission / no premium).\n // Adapter treats them differently \u2014 INVALID_API_KEY says \"fix the key\",\n // FORBIDDEN says \"fix the account\".\n const code: ApiErrorCode = statusCode === 401 ? \"INVALID_API_KEY\" : statusCode === 403 ? \"FORBIDDEN\" : \"HTTP_ERROR\";\n return apiError(`HTTP ${statusCode}: ${statusMessage}`, code);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["_a", "_b"]
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -116,8 +116,43 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
116
116
|
this.on("unload", this.onUnload.bind(this));
|
|
117
117
|
this.on("message", this.onMessage.bind(this));
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Switch off `supportedMessages.stopInstance` on this instance's own object.
|
|
121
|
+
*
|
|
122
|
+
* The entry was dropped from the manifest, which only helps a FRESH install: an upgrade
|
|
123
|
+
* merges the manifest into the existing instance object and never removes a key, so the old
|
|
124
|
+
* `true` survives in the database — and that is what the host reads. With it the host kills
|
|
125
|
+
* the process one second after asking it to stop, `onUnload` never runs, and the
|
|
126
|
+
* `info.connection=false` write on the way out never reaches the database.
|
|
127
|
+
*
|
|
128
|
+
* Only written when it is actually still on: every instance-object change restarts the
|
|
129
|
+
* instance, so doing it unconditionally would be a restart loop.
|
|
130
|
+
*
|
|
131
|
+
* @returns true when the correction was written and the restart is coming — the caller has
|
|
132
|
+
* to stop right there instead of polling in a process that is going down.
|
|
133
|
+
*/
|
|
134
|
+
async clearStopInstanceFlag() {
|
|
135
|
+
var _a;
|
|
136
|
+
const id = `system.adapter.${this.namespace}`;
|
|
137
|
+
try {
|
|
138
|
+
const obj = await this.getForeignObjectAsync(id);
|
|
139
|
+
const supported = (_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.supportedMessages;
|
|
140
|
+
if (!(supported == null ? void 0 : supported.stopInstance)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
this.log.info("Correcting a leftover setting from an earlier version \u2014 this instance restarts once");
|
|
144
|
+
await this.extendForeignObjectAsync(id, { common: { supportedMessages: { stopInstance: false } } });
|
|
145
|
+
return true;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
this.log.debug(`Could not check the instance object ${id}: ${(0, import_coerce.errText)(err)}`);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
119
151
|
async onReady() {
|
|
120
152
|
try {
|
|
153
|
+
if (await this.clearStopInstanceFlag()) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
121
156
|
await import_adapter_core.I18n.init((0, import_node_path.join)(this.adapterDir, "admin"), this);
|
|
122
157
|
this.log.debug(`onReady: starting (autoRemoveDelivered=${this.config.autoRemoveDelivered})`);
|
|
123
158
|
await this.setState("info.connection", { val: false, ack: true });
|
|
@@ -173,13 +208,13 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
173
208
|
}
|
|
174
209
|
this.testClients.clear();
|
|
175
210
|
void this.setState("info.connection", { val: false, ack: true }).catch(() => {
|
|
176
|
-
});
|
|
211
|
+
}).finally(callback);
|
|
212
|
+
return;
|
|
177
213
|
} catch (err) {
|
|
178
214
|
try {
|
|
179
215
|
this.log.debug(`onUnload error (ignored): ${(0, import_coerce.errText)(err)}`);
|
|
180
216
|
} catch {
|
|
181
217
|
}
|
|
182
|
-
} finally {
|
|
183
218
|
callback();
|
|
184
219
|
}
|
|
185
220
|
}
|
|
@@ -224,9 +259,8 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
224
259
|
* @param obj The sendTo message (validated: command + callback present)
|
|
225
260
|
*/
|
|
226
261
|
async handleCheckConnection(obj) {
|
|
227
|
-
var _a;
|
|
228
262
|
const msg = obj.message;
|
|
229
|
-
const key = (
|
|
263
|
+
const key = typeof (msg == null ? void 0 : msg.apiKey) === "string" ? msg.apiKey.trim() : "";
|
|
230
264
|
if (!key || key.length < MIN_API_KEY_LENGTH) {
|
|
231
265
|
this.log.debug("checkConnection: apiKey too short");
|
|
232
266
|
this.sendTo(obj.from, obj.command, { error: "API key is too short" }, obj.callback);
|
|
@@ -385,7 +419,11 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
385
419
|
this.log.info("Connection restored");
|
|
386
420
|
this.lastErrorCode = "";
|
|
387
421
|
}
|
|
388
|
-
|
|
422
|
+
try {
|
|
423
|
+
await this.setStateChangedAsync("info.connection", { val: true, ack: true });
|
|
424
|
+
} catch (err) {
|
|
425
|
+
this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${(0, import_coerce.errText)(err)}`);
|
|
426
|
+
}
|
|
389
427
|
const activeDeliveries = deliveries.filter((d) => stateManager.parseStatus(d) !== import_types.DELIVERED_STATUS_CODE);
|
|
390
428
|
const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;
|
|
391
429
|
stateManager.resetPollState();
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { coerceClampedInt, errText, isTrueish, oneLine } from \"./lib/coerce\";\nimport { ParcelClient, RETRY_AFTER_DEFAULT_SEC, RETRY_AFTER_MAX_SEC } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport { DELIVERED_STATUS_CODE } from \"./lib/types\";\nimport type { AddDeliveryRequest } from \"./lib/types\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\n// Minimum 60s between polls. Also the natural pacing after addDelivery: a\n// typical single add still polls immediately (the last poll is >60s ago),\n// while a burst of adds collapses to at most one extra GET per minute.\nconst MIN_POLL_GAP_MS = 60_000;\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n// v0.9.0 (S5): cap addDelivery field lengths. A sendTo caller is local, but a\n// runaway script must not push a multi-MB POST body to parcel.app. Identifiers\n// are short; this is generous for a description.\nconst MAX_ADD_FIELD_LEN = 512;\n// v0.9.0 (S3): cap the per-poll broker fan-out. Updates run in batches of this\n// size instead of all deliveries at once, so an abnormally large API response\n// can't flood the broker with thousands of concurrent writes.\nconst UPDATE_BATCH_SIZE = 25;\n// v0.9.0 (S4): client-side throttle on addDelivery POSTs. parcel.app enforces\n// ~20 POST/day server-side; this caps a runaway/buggy script's burst so it can't\n// hammer the API. The window is generous enough never to block a real batch-add\n// (the daily server limit is the real cap).\nconst MAX_ADDS_PER_WINDOW = 20;\nconst ADD_WINDOW_MS = 60_000;\n\n/**\n * Node transport-level error codes treated as (transient) NETWORK problems:\n * DNS, connection refused/reset, unreachable \u2014 plus EPIPE/ECONNABORTED/EPROTO\n * (v0.10.0, I9), which belong to the same warn+keep-retrying class.\n */\nconst NETWORK_ERROR_CODES = new Set([\n \"ENOTFOUND\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENETUNREACH\",\n \"EHOSTUNREACH\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"ECONNABORTED\",\n \"EPROTO\",\n]);\n\n/**\n * v0.10.0 (L12): the seam contracts name exactly the members main uses \u2014\n * structural Picks let the orchestration tests inject plain object fakes\n * without unsafe double-casts, and the compiler documents what a fake must\n * provide.\n */\ntype ClientLike = Pick<\n ParcelClient,\n \"getDeliveries\" | \"getCarrierName\" | \"addDelivery\" | \"testConnection\" | \"cancelAll\"\n>;\ntype StateManagerLike = Pick<\n StateManager,\n \"resetPollState\" | \"packageId\" | \"parseStatus\" | \"updateDelivery\" | \"updateSummary\" | \"cleanupDeliveries\"\n>;\n\n/**\n * ioBroker adapter for parcel.app package tracking. Exported so the\n * orchestration unit tests can drive its lifecycle/poll handlers directly.\n */\nexport class ParcelappAdapter extends utils.Adapter {\n private client: ClientLike | null = null;\n private stateManager: StateManagerLike | null = null;\n /**\n * Factories for the HTTP client + state manager \u2014 default to the real\n * constructors. Test seams (fleet pattern): unit tests replace these with\n * fakes to exercise the poll orchestration (throttle/rate-limit interplay,\n * error routing, failure dedup) without real network.\n *\n * @param apiKey parcel.app API key\n */\n private makeClient: (apiKey: string) => ClientLike = apiKey =>\n new ParcelClient(apiKey, { debug: (m: string) => this.log.debug(m) });\n private makeStateManager: () => StateManagerLike = () => new StateManager(this);\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n /**\n * v0.10.0 (L2): set in onUnload. onReady checks it after its awaits so a\n * stop during the first poll can no longer arm the interval or log\n * \"started\" after the unload already ran; batch failures during shutdown\n * degrade to debug.\n */\n private unloaded = false;\n /**\n * Package ids (not raw tracking numbers) whose last updateDelivery failed.\n * Keyed like the states so the dedup survives a sanitize collision or a\n * missing tracking number; pruned each poll against the visible pkgIds.\n */\n private failedDeliveries = new Set<string>();\n /** Timestamps of recent addDelivery POSTs \u2014 the S4 throttle window. */\n private addTimestamps: number[] = [];\n /**\n * L2: true while a checkConnection test GET is in flight. A test hits the same\n * 20/hour GET budget as polling; this guards against a concurrent second test\n * (double-click / admin re-render) stacking a redundant GET. A sequential\n * re-test after the current one settles runs normally.\n */\n private testConnectionInFlight = false;\n /**\n * v0.4.4: short-lived test-clients spawned from `checkConnection` admin\n * messages. The prod-`this.client` is what `onUnload` cancels, so these\n * need their own registry to be reachable at shutdown. Without this, an\n * admin clicking \"Test Connection\" right before adapter-stop could keep\n * the process alive past js-controller's 4-second kill deadline.\n */\n private testClients = new Set<ClientLike>();\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n }\n\n private async onReady(): Promise<void> {\n try {\n // I18n.init resolves system.config.language itself (adapter-core reads\n // the foreign object internally; unknown languages fall back to English)\n // \u2014 the former separate getForeignObject round-trip and the local\n // resolveLanguage step are gone with the STATUS_LABELS table (L20).\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.log.debug(`onReady: starting (autoRemoveDelivered=${this.config.autoRemoveDelivered})`);\n\n await this.setState(\"info.connection\", { val: false, ack: true });\n\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {\n this.log.error(\"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\");\n return;\n }\n\n this.client = this.makeClient(apiKey.trim());\n this.stateManager = this.makeStateManager();\n\n try {\n await this.cleanupObsoleteStates();\n } catch (err) {\n // C8: a cleanup failure must not abort startup. Without this guard the\n // outer catch would skip arming the poll interval below, and the adapter\n // would never poll until a manual restart. Degrade to a warning.\n this.log.warn(`cleanupObsoleteStates failed (continuing): ${errText(err)}`);\n }\n\n await this.poll();\n\n // v0.10.0 (L2): a stop during the awaits above must not arm a timer or\n // log a start line after onUnload already ran.\n if (this.unloaded) {\n return;\n }\n\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n this.log.debug(`pollInterval: raw=${JSON.stringify(this.config.pollInterval)} resolved=${interval}min`);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => {\n void this.poll().catch(err => this.log.error(`Scheduled poll failed: ${errText(err)}`));\n }, intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n // v0.10.0 (L4): a transient startup failure (i18n files, DB hiccup) used\n // to leave a green-looking zombie \u2014 no client, no timer, no retry until\n // a manual restart. Terminate instead: js-controller restarts the\n // instance, which self-heals the transient case; its restart-loop guard\n // backstops a persistent one.\n if (!this.unloaded) {\n this.terminate(\"startup failed \u2014 requesting restart\", utils.EXIT_CODES.START_IMMEDIATELY_AFTER_STOP);\n }\n }\n }\n\n /**\n * v0.4.2 (M5+X5): delegate to the shared `coerceClampedInt` helper.\n *\n * @param raw Raw `pollInterval` from admin config (number or numeric string).\n */\n private static coercePollInterval(raw: unknown): number {\n return coerceClampedInt(raw, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL, DEFAULT_POLL_INTERVAL);\n }\n\n private onUnload(callback: () => void): void {\n this.unloaded = true;\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n // v0.4.2 (M11+P1): cancel every in-flight HTTPS request so a slow\n // parcel.app endpoint doesn't keep the adapter alive past\n // js-controller's 4-second kill deadline. cancelAll also marks the\n // client terminal (v0.10.0, L3) \u2014 requests started after this point\n // are refused instead of opening fresh connections.\n this.client?.cancelAll();\n // v0.4.4: also abort any short-lived test-client (from checkConnection)\n // whose HTTPS-request might still be inflight at shutdown \u2014 the prod\n // `this.client.cancelAll()` only touches the production-client.\n for (const tc of this.testClients) {\n tc.cancelAll();\n }\n this.testClients.clear();\n // v0.4.2 (M10): explicit `.catch(() => {})` on the fire-and-forget so\n // a broker-already-down doesn't leak as an unhandled rejection.\n void this.setState(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker is shutting down \u2014 ignore */\n });\n } catch (err) {\n // v0.4.3 (G4): replace silent `// ignore` with a trace so shutdown\n // errors leave a debug breadcrumb. Broker-already-down errors here\n // are expected \u2014 debug-level keeps them out of the user log.\n try {\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n } catch {\n /* logger already gone \u2014 nothing left to report to */\n }\n } finally {\n // v0.10.0 (I7): the callback is the contract with js-controller \u2014\n // structurally guaranteed exactly once, even if the catch-path throws.\n callback();\n }\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise. Inside the try since\n // v0.10.0 (I6) so the handler body is gap-free top-level guarded.\n this.log.debug(\n `onMessage: command='${oneLine(String(obj?.command ?? \"\"))}' from='${obj?.from}' has-callback=${!!obj?.callback}`,\n );\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n switch (obj.command) {\n case \"checkConnection\":\n await this.handleCheckConnection(obj);\n break;\n case \"addDelivery\":\n await this.handleAddDelivery(obj);\n break;\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${oneLine(String(obj.command))}'`);\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n // v0.4.3 (F7): trace catch so the debug log shows what failed. The\n // reply itself is guarded too (v0.10.0, I6) \u2014 a synchronous sendTo\n // throw must not escape the message handler. checkConnection replies\n // use the admin {error} envelope (H1), scripts keep the documented\n // {success, error_message} shape.\n try {\n this.log.debug(`onMessage: '${oneLine(String(obj?.command ?? \"\"))}' failed: ${errText(err)}`);\n if (obj?.callback) {\n const reply =\n obj.command === \"checkConnection\"\n ? { error: errText(err) }\n : { success: false, error_message: errText(err) };\n this.sendTo(obj.from, obj.command, reply, obj.callback);\n }\n } catch {\n /* reply channel gone \u2014 nothing left to do */\n }\n }\n }\n\n /**\n * Admin \"Test Connection\" button (H1). The jsonConfig sendTo component reads\n * ONLY `response.error` / `response.result` \u2014 never success/message \u2014 so the\n * internal `{success, message}` result is mapped to that contract here.\n * Before this, a FAILED test showed a false-positive \"Ok\" in the admin\n * (fleet fix; beszel's message-router is the model).\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleCheckConnection(obj: ioBroker.Message): Promise<void> {\n const msg = obj.message as { apiKey?: string };\n const key = msg?.apiKey?.trim() || \"\";\n if (!key || key.length < MIN_API_KEY_LENGTH) {\n // v0.4.3 (F2): trace the reject before sendTo.\n this.log.debug(\"checkConnection: apiKey too short\");\n this.sendTo(obj.from, obj.command, { error: \"API key is too short\" }, obj.callback);\n return;\n }\n // L2: a Test-Connection GET counts against the same 20/hour budget as\n // polling. Guard against a concurrent second test (double-click / admin\n // re-render) so stacked clicks can't each burn a GET (which could later trip\n // the poll's rate-limit cooldown). Set BEFORE the await so the check is\n // synchronous against a still-in-flight first test.\n if (this.testConnectionInFlight) {\n this.log.debug(\"checkConnection: a test is already running\");\n this.sendTo(obj.from, obj.command, { error: \"A connection test is already running \u2014 please wait\" }, obj.callback);\n return;\n }\n this.testConnectionInFlight = true;\n // v0.4.3: same debug-logger as the prod client so checkConnection\n // failures get the same HTTPS-layer trace (via the makeClient seam).\n const testClient = this.makeClient(key);\n // v0.4.4: register test-client so onUnload can abort its inflight\n // HTTPS-request \u2014 the adapter's `this.client.cancelAll()` only\n // touches the prod-client, not these short-lived test-clients.\n this.testClients.add(testClient);\n try {\n const result = await testClient.testConnection();\n // v0.4.3 (F3): trace checkConnection result.\n this.log.debug(`checkConnection: result=${result.success ? \"ok\" : \"fail\"} (${result.message})`);\n this.sendTo(\n obj.from,\n obj.command,\n result.success ? { result: result.message } : { error: result.message },\n obj.callback,\n );\n } finally {\n this.testClients.delete(testClient);\n this.testConnectionInFlight = false;\n }\n }\n\n /**\n * Reply an addDelivery failure to the sendTo caller. This is the documented\n * script API envelope (`{success: false, error_message}`) \u2014 unchanged for\n * backward compatibility; only the admin checkConnection uses {error}.\n *\n * @param obj The sendTo message being answered\n * @param message Human-readable failure reason\n */\n private replyAddError(obj: ioBroker.Message, message: string): void {\n this.sendTo(obj.from, obj.command, { success: false, error_message: message }, obj.callback);\n }\n\n /**\n * Script-facing addDelivery command: validate the message shape, cap field\n * lengths, throttle bursts, forward to the API and trigger a poll on\n * success. Extracted from the onMessage switch (M9) \u2014 one command, one\n * method, one change reason.\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleAddDelivery(obj: ioBroker.Message): Promise<void> {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\n this.replyAddError(obj, \"Adapter not initialized\");\n return;\n }\n // v0.7.2: obj.message is `unknown`-shaped \u2014 a script calling\n // sendTo(\"parcelapp\", \"addDelivery\", null) used to surface as an\n // ugly TypeError through the catch instead of a clear validation\n // message. Coerce to a plain object and validate required fields.\n const raw = obj.message;\n const msg = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};\n if (\n typeof msg.tracking_number !== \"string\" ||\n msg.tracking_number.length === 0 ||\n typeof msg.carrier_code !== \"string\" ||\n msg.carrier_code.length === 0 ||\n typeof msg.description !== \"string\" ||\n msg.description.length === 0\n ) {\n this.log.debug(\"addDelivery: missing tracking_number/carrier_code/description in message\");\n this.replyAddError(obj, \"tracking_number, carrier_code and description are required\");\n return;\n }\n // v0.9.0 (S5): cap field lengths so a runaway local script can't push\n // a multi-MB POST body to parcel.app. Checked after the required-field\n // guard above so the two validation messages stay distinct.\n if (\n msg.tracking_number.length > MAX_ADD_FIELD_LEN ||\n msg.carrier_code.length > MAX_ADD_FIELD_LEN ||\n msg.description.length > MAX_ADD_FIELD_LEN ||\n (typeof msg.language === \"string\" && msg.language.length > MAX_ADD_FIELD_LEN)\n ) {\n this.log.debug(\"addDelivery: a field exceeds the maximum length\");\n this.replyAddError(obj, `each field must be at most ${MAX_ADD_FIELD_LEN} characters`);\n return;\n }\n // Pass the optional API fields through when the caller supplies them\n // (language: ISO 639-1 two-letter code; send_push_confirmation: push\n // notification once the delivery is added).\n const request: AddDeliveryRequest = {\n tracking_number: msg.tracking_number,\n carrier_code: msg.carrier_code,\n description: msg.description,\n };\n if (typeof msg.language === \"string\" && msg.language.length > 0) {\n request.language = msg.language;\n }\n if (typeof msg.send_push_confirmation === \"boolean\") {\n request.send_push_confirmation = msg.send_push_confirmation;\n }\n // v0.9.0 (S4): throttle addDelivery POSTs. parcel.app caps ~20/day\n // server-side; this stops a runaway/buggy script from hammering the API\n // with a burst. Record the attempt before the await so concurrent\n // callers count too.\n const nowMs = Date.now();\n this.addTimestamps = this.addTimestamps.filter(t => nowMs - t < ADD_WINDOW_MS);\n if (this.addTimestamps.length >= MAX_ADDS_PER_WINDOW) {\n this.log.warn(`addDelivery throttled: more than ${MAX_ADDS_PER_WINDOW} requests within ${ADD_WINDOW_MS / 1000}s`);\n this.replyAddError(obj, `too many addDelivery requests; max ${MAX_ADDS_PER_WINDOW} per ${ADD_WINDOW_MS / 1000}s`);\n return;\n }\n this.addTimestamps.push(nowMs);\n const addResult = await this.client.addDelivery(request);\n // v0.4.3 (F5): trace addDelivery result with the (flattened) tracking number.\n // v0.10.0 (L9): the drift-guarded isTrueish \u2014 getDeliveries hardens the same\n // API flag; a drifted `success: \"false\"` string must not read as truthy.\n const added = isTrueish(addResult.success);\n this.log.debug(`addDelivery: '${oneLine(request.tracking_number)}' result=${added ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (added) {\n // v0.10.0 (L5): plain poll, no force \u2014 a single add still polls right\n // away (the last poll is usually >60s back), but an add-burst can no\n // longer stack force-GETs past the 20/h API budget. Nothing is lost:\n // the server caches the list ~45-90 min, a fresh package rarely shows\n // tracking data immediately anyway.\n void this.poll().catch(err => this.log.error(`Poll after addDelivery failed: ${errText(err)}`));\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n // One getObject per adapter start, forever \u2014 deliberately kept (I3/ARCH-24):\n // a one-shot migration marker would cost more mechanics than this read.\n // Drop the list entirely at the next major once 0.1.x installs are gone.\n const obsoleteStates = [\n \"summary.json\", // removed in 0.2.0\n ];\n for (const stateId of obsoleteStates) {\n const obj = await this.getObjectAsync(stateId);\n if (obj) {\n await this.delObjectAsync(stateId);\n this.log.debug(`Removed obsolete state: ${stateId}`);\n }\n }\n }\n\n /**\n * Classify an error for deduplication and log-level decisions.\n *\n * v0.10.0 (M1): the client codes every failure it raises (TIMEOUT,\n * PARSE_ERROR, ABORTED, RATE_LIMITED, \u2026) \u2014 a present machine code always\n * wins. The message-substring sniff only remains for code-less foreign\n * errors, so an API error_message merely CONTAINING \"timeout\" can no longer\n * be misclassified.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code) {\n if (NETWORK_ERROR_CODES.has(error.code)) {\n return \"NETWORK\";\n }\n if (error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code;\n }\n if (error.message.includes(\"timeout\")) {\n return \"TIMEOUT\";\n }\n return \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n // v0.10.0 (M4): make the re-entry/uninitialized skip visible like the\n // rate-limit/throttle skips below \u2014 this used to be the one silent spot\n // where \"the adapter does nothing\" left no trace.\n this.log.debug(\"Skipping poll \u2014 already running or not initialized\");\n return;\n }\n // v0.10.0 (L14): local bindings instead of non-null assertions in the\n // closures below \u2014 provable narrowing the compiler checks, robust against\n // a future refactor nulling the fields mid-poll.\n const client = this.client;\n const stateManager = this.stateManager;\n\n const now = Date.now();\n // v0.4.3 (B1): poll-entry anchor \u2014 visible after the re-entry guard but\n // before the rate-limit/throttle skips. Shows mode + current error state\n // so the debug log gives context for whatever follows.\n const autoRemoveMode = this.config.autoRemoveDelivered !== false;\n this.log.debug(`poll: starting (autoRemove=${autoRemoveMode}, lastErrorCode='${this.lastErrorCode}')`);\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);\n return;\n }\n\n // Throttle: minimum gap between polls (also paces the poll after a\n // successful addDelivery \u2014 see handleAddDelivery).\n if (now - this.lastPollTime < MIN_POLL_GAP_MS) {\n this.log.debug(\"Skipping poll \u2014 too soon after last poll\");\n return;\n }\n\n this.isPolling = true;\n this.lastPollTime = now;\n try {\n // When keeping delivered packages, use \"recent\" to get them from API\n const deliveries = await client.getDeliveries(autoRemoveMode ? \"active\" : \"recent\");\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n await this.setStateChangedAsync(\"info.connection\", { val: true, ack: true });\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => stateManager.parseStatus(d) !== DELIVERED_STATUS_CODE);\n const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset the per-poll collision tracker, then compute every\n // package id in a deterministic sequential pre-pass (stable array order)\n // BEFORE the parallel updates \u2014 collision-suffixing is then deterministic\n // and packageId runs exactly once per delivery instead of twice.\n stateManager.resetPollState();\n const pkgIds = visibleDeliveries.map(d => stateManager.packageId(d));\n\n // v0.4.2 (M4): per-delivery updates run in parallel, each wrapped in\n // try/catch so one bad delivery doesn't poison the others.\n // v0.9.0 (S3): process the updates in bounded batches instead of one\n // broker fan-out for ALL deliveries at once. The keep-set is still every\n // visible pkgId (computed above), so this only caps concurrency \u2014 it never\n // drops a package. A normal poll (a handful of packages) is a single batch.\n if (visibleDeliveries.length > UPDATE_BATCH_SIZE) {\n this.log.debug(`Updating ${visibleDeliveries.length} deliveries in batches of ${UPDATE_BATCH_SIZE}`);\n }\n for (let start = 0; start < visibleDeliveries.length; start += UPDATE_BATCH_SIZE) {\n const batch = visibleDeliveries.slice(start, start + UPDATE_BATCH_SIZE);\n await Promise.all(\n batch.map(async (delivery, offset) => {\n const pkgId = pkgIds[start + offset];\n // Pre-sanitize the externally-sourced strings once for logging. The\n // fields are optional (the API can drop them), so default to \"\" before\n // oneLine flattens them onto a single log line.\n const tracking = oneLine(delivery.tracking_number ?? \"\");\n const carrier = oneLine(delivery.carrier_code ?? \"\");\n try {\n // v0.4.3 (C1): per-delivery entry. ~10 packages \u00D7 144 polls/day\n // = ~1440 debug lines/day \u2014 acceptable at debug-level. Line stays\n // short (tracking + carrier + status only, no full delivery JSON).\n // v0.10.0 (M6): status_code is typed number|string (drift), so it\n // is flattened like the other externally-sourced fields.\n this.log.debug(\n `updateDelivery: '${tracking}' carrier=${carrier} status=${oneLine(String(delivery.status_code))}`,\n );\n const carrierName = await client.getCarrierName(delivery.carrier_code);\n await stateManager.updateDelivery(delivery, carrierName, pkgId);\n this.failedDeliveries.delete(pkgId);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(pkgId)) {\n this.log.debug(`Failed to update '${tracking}': ${msg}`);\n } else if (this.unloaded) {\n // v0.10.0 (L2): broker teardown mid-batch is expected noise\n // during shutdown, not a per-package warning.\n this.log.debug(`Failed to update '${tracking}' during shutdown: ${msg}`);\n } else {\n this.log.warn(`Failed to update '${tracking}': ${msg}`);\n this.failedDeliveries.add(pkgId);\n }\n }\n }),\n );\n }\n\n // v0.9.0 (C1): keep-set = EVERY package the API still returns this poll\n // (pkgIds), NOT only the writes that just succeeded. A transient\n // updateDelivery failure leaves a package's states stale, but it must not\n // drop the package from cleanup \u2014 that would delete a still-present\n // package's states (and any user-set device name) at green info.connection.\n // v0.10.0 (M2): broker-side failures in cleanup/summary are NOT API\n // failures \u2014 they must neither flip info.connection to false (the GET\n // above just succeeded) nor run through the API error classification.\n try {\n await stateManager.cleanupDeliveries(pkgIds);\n // Update summary (always uses active/non-delivered)\n await stateManager.updateSummary(activeDeliveries);\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Keep failedDeliveries bounded: drop entries for package ids no longer\n // present, so packages that vanish from the API don't linger forever.\n const seenPkgIds = new Set(pkgIds);\n for (const id of [...this.failedDeliveries]) {\n if (!seenPkgIds.has(id)) {\n this.failedDeliveries.delete(id);\n }\n }\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n await this.handlePollError(err as Error & { code?: string; retryAfterSeconds?: number });\n } finally {\n this.isPolling = false;\n }\n }\n\n /**\n * Classify + route a poll failure: log level, dedup, cooldown and the\n * info.connection=false write. Extracted from poll()'s catch (M9) so the\n * happy path reads as a plain sequence and the error policy sits next to\n * classifyError. Dispatches on the CLASSIFIED code only (L6) \u2014 one source\n * of truth for the error class.\n *\n * @param error The poll failure (usually an ApiError from the client)\n */\n private async handlePollError(error: Error & { code?: string; retryAfterSeconds?: number }): Promise<void> {\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n switch (errorCode) {\n case \"ABORTED\":\n // v0.10.0 (M1): expected during shutdown \u2014 cancelAll aborts the\n // in-flight GET. A deliberate stop must not paint a red error line.\n this.log.debug(`Poll aborted: ${error.message}`);\n break;\n case \"RATE_LIMITED\": {\n // v0.4.2 (M9): clamp Retry-After into [60s, 24h] (shared constants\n // with the client parser, L7). A bogus 0/negative/fractional value\n // must neither wipe the cooldown nor set it for milliseconds.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(RETRY_AFTER_MAX_SEC, Math.max(60, Math.floor(rawCooldown)))\n : RETRY_AFTER_DEFAULT_SEC;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n // v0.10.0 (M3): warn once \u2014 a persistent 429 repeats at debug.\n const line = `Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`;\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.warn(line);\n }\n break;\n }\n case \"FORBIDDEN\": {\n // v0.4.2 (P3): 403 is a permission issue (e.g. Premium subscription\n // expired). Reauth wouldn't help \u2014 surface a clear hint.\n // v0.10.0 (M3): once at error level, repeats at debug \u2014 not 144\n // identical error lines per day for one unchanged account problem.\n const line =\n \"parcel.app returned 403 Forbidden \u2014 your account may not have an active Premium subscription, or the API key was revoked. Check your account on parcelapp.net.\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"INVALID_API_KEY\": {\n // v0.10.0 (M3): first occurrence at error (the user must fix the\n // config; info.connection goes red too) \u2014 repeats at debug.\n const line = \"Invalid API key \u2014 please check your parcel.app API key\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"NETWORK\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n }\n break;\n case \"TIMEOUT\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n }\n break;\n default:\n if (isRepeat) {\n // Same error as last time \u2014 don't spam the log\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.error(`Poll failed: ${error.message}`);\n }\n }\n\n // C2: setStateChangedAsync avoids redundant `false` writes on sustained\n // failure. The `.catch` keeps poll() from rejecting when the broker is\n // already down, so the fire-and-forget callers never see an unhandled\n // rejection (no global process handler needed).\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker shutting down \u2014 ignore */\n });\n }\n}\n\n// Process entry point. Not reachable from the unit tests (they import the class\n// instead of running the file as a process), but NOT untested: `npm run\n// test:integration` boots the adapter through exactly this branch inside a real\n// js-controller, which is what catches module-scope startup crashes.\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { coerceClampedInt, errText, isTrueish, oneLine } from \"./lib/coerce\";\nimport { ParcelClient, RETRY_AFTER_DEFAULT_SEC, RETRY_AFTER_MAX_SEC } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport { DELIVERED_STATUS_CODE } from \"./lib/types\";\nimport type { AddDeliveryRequest } from \"./lib/types\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\n// Minimum 60s between polls. Also the natural pacing after addDelivery: a\n// typical single add still polls immediately (the last poll is >60s ago),\n// while a burst of adds collapses to at most one extra GET per minute.\nconst MIN_POLL_GAP_MS = 60_000;\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n// v0.9.0 (S5): cap addDelivery field lengths. A sendTo caller is local, but a\n// runaway script must not push a multi-MB POST body to parcel.app. Identifiers\n// are short; this is generous for a description.\nconst MAX_ADD_FIELD_LEN = 512;\n// v0.9.0 (S3): cap the per-poll broker fan-out. Updates run in batches of this\n// size instead of all deliveries at once, so an abnormally large API response\n// can't flood the broker with thousands of concurrent writes.\nconst UPDATE_BATCH_SIZE = 25;\n// v0.9.0 (S4): client-side throttle on addDelivery POSTs. parcel.app enforces\n// ~20 POST/day server-side; this caps a runaway/buggy script's burst so it can't\n// hammer the API. The window is generous enough never to block a real batch-add\n// (the daily server limit is the real cap).\nconst MAX_ADDS_PER_WINDOW = 20;\nconst ADD_WINDOW_MS = 60_000;\n\n/**\n * Node transport-level error codes treated as (transient) NETWORK problems:\n * DNS, connection refused/reset, unreachable \u2014 plus EPIPE/ECONNABORTED/EPROTO\n * (v0.10.0, I9), which belong to the same warn+keep-retrying class.\n */\nconst NETWORK_ERROR_CODES = new Set([\n \"ENOTFOUND\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENETUNREACH\",\n \"EHOSTUNREACH\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"ECONNABORTED\",\n \"EPROTO\",\n]);\n\n/**\n * v0.10.0 (L12): the seam contracts name exactly the members main uses \u2014\n * structural Picks let the orchestration tests inject plain object fakes\n * without unsafe double-casts, and the compiler documents what a fake must\n * provide.\n */\ntype ClientLike = Pick<\n ParcelClient,\n \"getDeliveries\" | \"getCarrierName\" | \"addDelivery\" | \"testConnection\" | \"cancelAll\"\n>;\ntype StateManagerLike = Pick<\n StateManager,\n \"resetPollState\" | \"packageId\" | \"parseStatus\" | \"updateDelivery\" | \"updateSummary\" | \"cleanupDeliveries\"\n>;\n\n/**\n * ioBroker adapter for parcel.app package tracking. Exported so the\n * orchestration unit tests can drive its lifecycle/poll handlers directly.\n */\nexport class ParcelappAdapter extends utils.Adapter {\n private client: ClientLike | null = null;\n private stateManager: StateManagerLike | null = null;\n /**\n * Factories for the HTTP client + state manager \u2014 default to the real\n * constructors. Test seams (fleet pattern): unit tests replace these with\n * fakes to exercise the poll orchestration (throttle/rate-limit interplay,\n * error routing, failure dedup) without real network.\n *\n * @param apiKey parcel.app API key\n */\n private makeClient: (apiKey: string) => ClientLike = apiKey =>\n new ParcelClient(apiKey, { debug: (m: string) => this.log.debug(m) });\n private makeStateManager: () => StateManagerLike = () => new StateManager(this);\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n /**\n * v0.10.0 (L2): set in onUnload. onReady checks it after its awaits so a\n * stop during the first poll can no longer arm the interval or log\n * \"started\" after the unload already ran; batch failures during shutdown\n * degrade to debug.\n */\n private unloaded = false;\n /**\n * Package ids (not raw tracking numbers) whose last updateDelivery failed.\n * Keyed like the states so the dedup survives a sanitize collision or a\n * missing tracking number; pruned each poll against the visible pkgIds.\n */\n private failedDeliveries = new Set<string>();\n /** Timestamps of recent addDelivery POSTs \u2014 the S4 throttle window. */\n private addTimestamps: number[] = [];\n /**\n * L2: true while a checkConnection test GET is in flight. A test hits the same\n * 20/hour GET budget as polling; this guards against a concurrent second test\n * (double-click / admin re-render) stacking a redundant GET. A sequential\n * re-test after the current one settles runs normally.\n */\n private testConnectionInFlight = false;\n /**\n * v0.4.4: short-lived test-clients spawned from `checkConnection` admin\n * messages. The prod-`this.client` is what `onUnload` cancels, so these\n * need their own registry to be reachable at shutdown. Without this, an\n * admin clicking \"Test Connection\" right before adapter-stop could keep\n * the process alive past js-controller's 4-second kill deadline.\n */\n private testClients = new Set<ClientLike>();\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n }\n\n /**\n * Switch off `supportedMessages.stopInstance` on this instance's own object.\n *\n * The entry was dropped from the manifest, which only helps a FRESH install: an upgrade\n * merges the manifest into the existing instance object and never removes a key, so the old\n * `true` survives in the database \u2014 and that is what the host reads. With it the host kills\n * the process one second after asking it to stop, `onUnload` never runs, and the\n * `info.connection=false` write on the way out never reaches the database.\n *\n * Only written when it is actually still on: every instance-object change restarts the\n * instance, so doing it unconditionally would be a restart loop.\n *\n * @returns true when the correction was written and the restart is coming \u2014 the caller has\n * to stop right there instead of polling in a process that is going down.\n */\n private async clearStopInstanceFlag(): Promise<boolean> {\n const id = `system.adapter.${this.namespace}`;\n try {\n const obj = await this.getForeignObjectAsync(id);\n const supported = obj?.common?.supportedMessages as { stopInstance?: unknown } | undefined;\n if (!supported?.stopInstance) {\n return false;\n }\n this.log.info(\"Correcting a leftover setting from an earlier version \u2014 this instance restarts once\");\n await this.extendForeignObjectAsync(id, { common: { supportedMessages: { stopInstance: false } } });\n return true;\n } catch (err) {\n // Objects DB unreachable \u2014 not worth failing the start over; the next start retries.\n this.log.debug(`Could not check the instance object ${id}: ${errText(err)}`);\n return false;\n }\n }\n\n private async onReady(): Promise<void> {\n try {\n // First: without this the whole shutdown path stays dead on an updated install.\n // A correction means the host is restarting us \u2014 no point starting a poll cycle.\n if (await this.clearStopInstanceFlag()) {\n return;\n }\n // I18n.init resolves system.config.language itself (adapter-core reads\n // the foreign object internally; unknown languages fall back to English)\n // \u2014 the former separate getForeignObject round-trip and the local\n // resolveLanguage step are gone with the STATUS_LABELS table (L20).\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.log.debug(`onReady: starting (autoRemoveDelivered=${this.config.autoRemoveDelivered})`);\n\n await this.setState(\"info.connection\", { val: false, ack: true });\n\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {\n this.log.error(\"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\");\n return;\n }\n\n this.client = this.makeClient(apiKey.trim());\n this.stateManager = this.makeStateManager();\n\n try {\n await this.cleanupObsoleteStates();\n } catch (err) {\n // C8: a cleanup failure must not abort startup. Without this guard the\n // outer catch would skip arming the poll interval below, and the adapter\n // would never poll until a manual restart. Degrade to a warning.\n this.log.warn(`cleanupObsoleteStates failed (continuing): ${errText(err)}`);\n }\n\n await this.poll();\n\n // v0.10.0 (L2): a stop during the awaits above must not arm a timer or\n // log a start line after onUnload already ran.\n if (this.unloaded) {\n return;\n }\n\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n this.log.debug(`pollInterval: raw=${JSON.stringify(this.config.pollInterval)} resolved=${interval}min`);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => {\n void this.poll().catch(err => this.log.error(`Scheduled poll failed: ${errText(err)}`));\n }, intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n // v0.10.0 (L4): a transient startup failure (i18n files, DB hiccup) used\n // to leave a green-looking zombie \u2014 no client, no timer, no retry until\n // a manual restart. Terminate instead: js-controller restarts the\n // instance, which self-heals the transient case; its restart-loop guard\n // backstops a persistent one.\n if (!this.unloaded) {\n this.terminate(\"startup failed \u2014 requesting restart\", utils.EXIT_CODES.START_IMMEDIATELY_AFTER_STOP);\n }\n }\n }\n\n /**\n * v0.4.2 (M5+X5): delegate to the shared `coerceClampedInt` helper.\n *\n * @param raw Raw `pollInterval` from admin config (number or numeric string).\n */\n private static coercePollInterval(raw: unknown): number {\n return coerceClampedInt(raw, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL, DEFAULT_POLL_INTERVAL);\n }\n\n private onUnload(callback: () => void): void {\n this.unloaded = true;\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n // v0.4.2 (M11+P1): cancel every in-flight HTTPS request so a slow\n // parcel.app endpoint doesn't keep the adapter alive past\n // js-controller's 4-second kill deadline. cancelAll also marks the\n // client terminal (v0.10.0, L3) \u2014 requests started after this point\n // are refused instead of opening fresh connections.\n this.client?.cancelAll();\n // v0.4.4: also abort any short-lived test-client (from checkConnection)\n // whose HTTPS-request might still be inflight at shutdown \u2014 the prod\n // `this.client.cancelAll()` only touches the production-client.\n for (const tc of this.testClients) {\n tc.cancelAll();\n }\n this.testClients.clear();\n // v0.10.3: report done only once the disconnect write landed. Calling back\n // first means the host tears the process down while the write is still in\n // flight and the instance keeps claiming a live connection. No own deadline\n // needed \u2014 the host already has one (`common.stopTimeout`), and\n // `this.setTimeout` refuses during shutdown anyway.\n void this.setState(\"info.connection\", { val: false, ack: true })\n .catch(() => {\n /* broker is shutting down \u2014 ignore */\n })\n .finally(callback);\n return;\n } catch (err) {\n // v0.4.3 (G4): replace silent `// ignore` with a trace so shutdown\n // errors leave a debug breadcrumb. Broker-already-down errors here\n // are expected \u2014 debug-level keeps them out of the user log.\n try {\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n } catch {\n /* logger already gone \u2014 nothing left to report to */\n }\n // v0.10.0 (I7): the callback is the contract with js-controller \u2014 exactly\n // once. v0.10.3: the happy path returns above with the callback chained to\n // the disconnect write, so this catch is the only other exit.\n callback();\n }\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise. Inside the try since\n // v0.10.0 (I6) so the handler body is gap-free top-level guarded.\n this.log.debug(\n `onMessage: command='${oneLine(String(obj?.command ?? \"\"))}' from='${obj?.from}' has-callback=${!!obj?.callback}`,\n );\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n switch (obj.command) {\n case \"checkConnection\":\n await this.handleCheckConnection(obj);\n break;\n case \"addDelivery\":\n await this.handleAddDelivery(obj);\n break;\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${oneLine(String(obj.command))}'`);\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n // v0.4.3 (F7): trace catch so the debug log shows what failed. The\n // reply itself is guarded too (v0.10.0, I6) \u2014 a synchronous sendTo\n // throw must not escape the message handler. checkConnection replies\n // use the admin {error} envelope (H1), scripts keep the documented\n // {success, error_message} shape.\n try {\n this.log.debug(`onMessage: '${oneLine(String(obj?.command ?? \"\"))}' failed: ${errText(err)}`);\n if (obj?.callback) {\n const reply =\n obj.command === \"checkConnection\"\n ? { error: errText(err) }\n : { success: false, error_message: errText(err) };\n this.sendTo(obj.from, obj.command, reply, obj.callback);\n }\n } catch {\n /* reply channel gone \u2014 nothing left to do */\n }\n }\n }\n\n /**\n * Admin \"Test Connection\" button (H1). The jsonConfig sendTo component reads\n * ONLY `response.error` / `response.result` \u2014 never success/message \u2014 so the\n * internal `{success, message}` result is mapped to that contract here.\n * Before this, a FAILED test showed a false-positive \"Ok\" in the admin\n * (fleet fix; beszel's message-router is the model).\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleCheckConnection(obj: ioBroker.Message): Promise<void> {\n const msg = obj.message as { apiKey?: unknown } | null | undefined;\n // API-boundary guard: a script may put anything here \u2014 a number used to\n // surface as \"trim is not a function\" instead of the plain rejection.\n const key = typeof msg?.apiKey === \"string\" ? msg.apiKey.trim() : \"\";\n if (!key || key.length < MIN_API_KEY_LENGTH) {\n // v0.4.3 (F2): trace the reject before sendTo.\n this.log.debug(\"checkConnection: apiKey too short\");\n this.sendTo(obj.from, obj.command, { error: \"API key is too short\" }, obj.callback);\n return;\n }\n // L2: a Test-Connection GET counts against the same 20/hour budget as\n // polling. Guard against a concurrent second test (double-click / admin\n // re-render) so stacked clicks can't each burn a GET (which could later trip\n // the poll's rate-limit cooldown). Set BEFORE the await so the check is\n // synchronous against a still-in-flight first test.\n if (this.testConnectionInFlight) {\n this.log.debug(\"checkConnection: a test is already running\");\n this.sendTo(obj.from, obj.command, { error: \"A connection test is already running \u2014 please wait\" }, obj.callback);\n return;\n }\n this.testConnectionInFlight = true;\n // v0.4.3: same debug-logger as the prod client so checkConnection\n // failures get the same HTTPS-layer trace (via the makeClient seam).\n const testClient = this.makeClient(key);\n // v0.4.4: register test-client so onUnload can abort its inflight\n // HTTPS-request \u2014 the adapter's `this.client.cancelAll()` only\n // touches the prod-client, not these short-lived test-clients.\n this.testClients.add(testClient);\n try {\n const result = await testClient.testConnection();\n // v0.4.3 (F3): trace checkConnection result.\n this.log.debug(`checkConnection: result=${result.success ? \"ok\" : \"fail\"} (${result.message})`);\n this.sendTo(\n obj.from,\n obj.command,\n result.success ? { result: result.message } : { error: result.message },\n obj.callback,\n );\n } finally {\n this.testClients.delete(testClient);\n this.testConnectionInFlight = false;\n }\n }\n\n /**\n * Reply an addDelivery failure to the sendTo caller. This is the documented\n * script API envelope (`{success: false, error_message}`) \u2014 unchanged for\n * backward compatibility; only the admin checkConnection uses {error}.\n *\n * @param obj The sendTo message being answered\n * @param message Human-readable failure reason\n */\n private replyAddError(obj: ioBroker.Message, message: string): void {\n this.sendTo(obj.from, obj.command, { success: false, error_message: message }, obj.callback);\n }\n\n /**\n * Script-facing addDelivery command: validate the message shape, cap field\n * lengths, throttle bursts, forward to the API and trigger a poll on\n * success. Extracted from the onMessage switch (M9) \u2014 one command, one\n * method, one change reason.\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleAddDelivery(obj: ioBroker.Message): Promise<void> {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\n this.replyAddError(obj, \"Adapter not initialized\");\n return;\n }\n // v0.7.2: obj.message is `unknown`-shaped \u2014 a script calling\n // sendTo(\"parcelapp\", \"addDelivery\", null) used to surface as an\n // ugly TypeError through the catch instead of a clear validation\n // message. Coerce to a plain object and validate required fields.\n const raw = obj.message;\n const msg = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};\n if (\n typeof msg.tracking_number !== \"string\" ||\n msg.tracking_number.length === 0 ||\n typeof msg.carrier_code !== \"string\" ||\n msg.carrier_code.length === 0 ||\n typeof msg.description !== \"string\" ||\n msg.description.length === 0\n ) {\n this.log.debug(\"addDelivery: missing tracking_number/carrier_code/description in message\");\n this.replyAddError(obj, \"tracking_number, carrier_code and description are required\");\n return;\n }\n // v0.9.0 (S5): cap field lengths so a runaway local script can't push\n // a multi-MB POST body to parcel.app. Checked after the required-field\n // guard above so the two validation messages stay distinct.\n if (\n msg.tracking_number.length > MAX_ADD_FIELD_LEN ||\n msg.carrier_code.length > MAX_ADD_FIELD_LEN ||\n msg.description.length > MAX_ADD_FIELD_LEN ||\n (typeof msg.language === \"string\" && msg.language.length > MAX_ADD_FIELD_LEN)\n ) {\n this.log.debug(\"addDelivery: a field exceeds the maximum length\");\n this.replyAddError(obj, `each field must be at most ${MAX_ADD_FIELD_LEN} characters`);\n return;\n }\n // Pass the optional API fields through when the caller supplies them\n // (language: ISO 639-1 two-letter code; send_push_confirmation: push\n // notification once the delivery is added).\n const request: AddDeliveryRequest = {\n tracking_number: msg.tracking_number,\n carrier_code: msg.carrier_code,\n description: msg.description,\n };\n if (typeof msg.language === \"string\" && msg.language.length > 0) {\n request.language = msg.language;\n }\n if (typeof msg.send_push_confirmation === \"boolean\") {\n request.send_push_confirmation = msg.send_push_confirmation;\n }\n // v0.9.0 (S4): throttle addDelivery POSTs. parcel.app caps ~20/day\n // server-side; this stops a runaway/buggy script from hammering the API\n // with a burst. Record the attempt before the await so concurrent\n // callers count too.\n const nowMs = Date.now();\n this.addTimestamps = this.addTimestamps.filter(t => nowMs - t < ADD_WINDOW_MS);\n if (this.addTimestamps.length >= MAX_ADDS_PER_WINDOW) {\n this.log.warn(`addDelivery throttled: more than ${MAX_ADDS_PER_WINDOW} requests within ${ADD_WINDOW_MS / 1000}s`);\n this.replyAddError(obj, `too many addDelivery requests; max ${MAX_ADDS_PER_WINDOW} per ${ADD_WINDOW_MS / 1000}s`);\n return;\n }\n this.addTimestamps.push(nowMs);\n const addResult = await this.client.addDelivery(request);\n // v0.4.3 (F5): trace addDelivery result with the (flattened) tracking number.\n // v0.10.0 (L9): the drift-guarded isTrueish \u2014 getDeliveries hardens the same\n // API flag; a drifted `success: \"false\"` string must not read as truthy.\n const added = isTrueish(addResult.success);\n this.log.debug(`addDelivery: '${oneLine(request.tracking_number)}' result=${added ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (added) {\n // v0.10.0 (L5): plain poll, no force \u2014 a single add still polls right\n // away (the last poll is usually >60s back), but an add-burst can no\n // longer stack force-GETs past the 20/h API budget. Nothing is lost:\n // the server caches the list ~45-90 min, a fresh package rarely shows\n // tracking data immediately anyway.\n void this.poll().catch(err => this.log.error(`Poll after addDelivery failed: ${errText(err)}`));\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n // One getObject per adapter start, forever \u2014 deliberately kept (I3/ARCH-24):\n // a one-shot migration marker would cost more mechanics than this read.\n // Drop the list entirely at the next major once 0.1.x installs are gone.\n const obsoleteStates = [\n \"summary.json\", // removed in 0.2.0\n ];\n for (const stateId of obsoleteStates) {\n const obj = await this.getObjectAsync(stateId);\n if (obj) {\n await this.delObjectAsync(stateId);\n this.log.debug(`Removed obsolete state: ${stateId}`);\n }\n }\n }\n\n /**\n * Classify an error for deduplication and log-level decisions.\n *\n * v0.10.0 (M1): the client codes every failure it raises (TIMEOUT,\n * PARSE_ERROR, ABORTED, RATE_LIMITED, \u2026) \u2014 a present machine code always\n * wins. The message-substring sniff only remains for code-less foreign\n * errors, so an API error_message merely CONTAINING \"timeout\" can no longer\n * be misclassified.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code) {\n if (NETWORK_ERROR_CODES.has(error.code)) {\n return \"NETWORK\";\n }\n if (error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code;\n }\n if (error.message.includes(\"timeout\")) {\n return \"TIMEOUT\";\n }\n return \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n // v0.10.0 (M4): make the re-entry/uninitialized skip visible like the\n // rate-limit/throttle skips below \u2014 this used to be the one silent spot\n // where \"the adapter does nothing\" left no trace.\n this.log.debug(\"Skipping poll \u2014 already running or not initialized\");\n return;\n }\n // v0.10.0 (L14): local bindings instead of non-null assertions in the\n // closures below \u2014 provable narrowing the compiler checks, robust against\n // a future refactor nulling the fields mid-poll.\n const client = this.client;\n const stateManager = this.stateManager;\n\n const now = Date.now();\n // v0.4.3 (B1): poll-entry anchor \u2014 visible after the re-entry guard but\n // before the rate-limit/throttle skips. Shows mode + current error state\n // so the debug log gives context for whatever follows.\n const autoRemoveMode = this.config.autoRemoveDelivered !== false;\n this.log.debug(`poll: starting (autoRemove=${autoRemoveMode}, lastErrorCode='${this.lastErrorCode}')`);\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);\n return;\n }\n\n // Throttle: minimum gap between polls (also paces the poll after a\n // successful addDelivery \u2014 see handleAddDelivery).\n if (now - this.lastPollTime < MIN_POLL_GAP_MS) {\n this.log.debug(\"Skipping poll \u2014 too soon after last poll\");\n return;\n }\n\n this.isPolling = true;\n this.lastPollTime = now;\n try {\n // When keeping delivered packages, use \"recent\" to get them from API\n const deliveries = await client.getDeliveries(autoRemoveMode ? \"active\" : \"recent\");\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n // The GET succeeded \u2014 from here on a failure is the broker's, not the\n // API's. v0.10.0 (M2) fenced cleanup/summary that way but left this write\n // outside: a broker hiccup here painted a \"Poll failed\" error line, flipped\n // the indicator to false and poisoned the error dedup.\n try {\n await this.setStateChangedAsync(\"info.connection\", { val: true, ack: true });\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => stateManager.parseStatus(d) !== DELIVERED_STATUS_CODE);\n const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset the per-poll collision tracker, then compute every\n // package id in a deterministic sequential pre-pass (stable array order)\n // BEFORE the parallel updates \u2014 collision-suffixing is then deterministic\n // and packageId runs exactly once per delivery instead of twice.\n stateManager.resetPollState();\n const pkgIds = visibleDeliveries.map(d => stateManager.packageId(d));\n\n // v0.4.2 (M4): per-delivery updates run in parallel, each wrapped in\n // try/catch so one bad delivery doesn't poison the others.\n // v0.9.0 (S3): process the updates in bounded batches instead of one\n // broker fan-out for ALL deliveries at once. The keep-set is still every\n // visible pkgId (computed above), so this only caps concurrency \u2014 it never\n // drops a package. A normal poll (a handful of packages) is a single batch.\n if (visibleDeliveries.length > UPDATE_BATCH_SIZE) {\n this.log.debug(`Updating ${visibleDeliveries.length} deliveries in batches of ${UPDATE_BATCH_SIZE}`);\n }\n for (let start = 0; start < visibleDeliveries.length; start += UPDATE_BATCH_SIZE) {\n const batch = visibleDeliveries.slice(start, start + UPDATE_BATCH_SIZE);\n await Promise.all(\n batch.map(async (delivery, offset) => {\n const pkgId = pkgIds[start + offset];\n // Pre-sanitize the externally-sourced strings once for logging. The\n // fields are optional (the API can drop them), so default to \"\" before\n // oneLine flattens them onto a single log line.\n const tracking = oneLine(delivery.tracking_number ?? \"\");\n const carrier = oneLine(delivery.carrier_code ?? \"\");\n try {\n // v0.4.3 (C1): per-delivery entry. ~10 packages \u00D7 144 polls/day\n // = ~1440 debug lines/day \u2014 acceptable at debug-level. Line stays\n // short (tracking + carrier + status only, no full delivery JSON).\n // v0.10.0 (M6): status_code is typed number|string (drift), so it\n // is flattened like the other externally-sourced fields.\n this.log.debug(\n `updateDelivery: '${tracking}' carrier=${carrier} status=${oneLine(String(delivery.status_code))}`,\n );\n const carrierName = await client.getCarrierName(delivery.carrier_code);\n await stateManager.updateDelivery(delivery, carrierName, pkgId);\n this.failedDeliveries.delete(pkgId);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(pkgId)) {\n this.log.debug(`Failed to update '${tracking}': ${msg}`);\n } else if (this.unloaded) {\n // v0.10.0 (L2): broker teardown mid-batch is expected noise\n // during shutdown, not a per-package warning.\n this.log.debug(`Failed to update '${tracking}' during shutdown: ${msg}`);\n } else {\n this.log.warn(`Failed to update '${tracking}': ${msg}`);\n this.failedDeliveries.add(pkgId);\n }\n }\n }),\n );\n }\n\n // v0.9.0 (C1): keep-set = EVERY package the API still returns this poll\n // (pkgIds), NOT only the writes that just succeeded. A transient\n // updateDelivery failure leaves a package's states stale, but it must not\n // drop the package from cleanup \u2014 that would delete a still-present\n // package's states (and any user-set device name) at green info.connection.\n // v0.10.0 (M2): broker-side failures in cleanup/summary are NOT API\n // failures \u2014 they must neither flip info.connection to false (the GET\n // above just succeeded) nor run through the API error classification.\n try {\n await stateManager.cleanupDeliveries(pkgIds);\n // Update summary (always uses active/non-delivered)\n await stateManager.updateSummary(activeDeliveries);\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Keep failedDeliveries bounded: drop entries for package ids no longer\n // present, so packages that vanish from the API don't linger forever.\n const seenPkgIds = new Set(pkgIds);\n for (const id of [...this.failedDeliveries]) {\n if (!seenPkgIds.has(id)) {\n this.failedDeliveries.delete(id);\n }\n }\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n await this.handlePollError(err as Error & { code?: string; retryAfterSeconds?: number });\n } finally {\n this.isPolling = false;\n }\n }\n\n /**\n * Classify + route a poll failure: log level, dedup, cooldown and the\n * info.connection=false write. Extracted from poll()'s catch (M9) so the\n * happy path reads as a plain sequence and the error policy sits next to\n * classifyError. Dispatches on the CLASSIFIED code only (L6) \u2014 one source\n * of truth for the error class.\n *\n * @param error The poll failure (usually an ApiError from the client)\n */\n private async handlePollError(error: Error & { code?: string; retryAfterSeconds?: number }): Promise<void> {\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n switch (errorCode) {\n case \"ABORTED\":\n // v0.10.0 (M1): expected during shutdown \u2014 cancelAll aborts the\n // in-flight GET. A deliberate stop must not paint a red error line.\n this.log.debug(`Poll aborted: ${error.message}`);\n break;\n case \"RATE_LIMITED\": {\n // v0.4.2 (M9): clamp Retry-After into [60s, 24h] (shared constants\n // with the client parser, L7). A bogus 0/negative/fractional value\n // must neither wipe the cooldown nor set it for milliseconds.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(RETRY_AFTER_MAX_SEC, Math.max(60, Math.floor(rawCooldown)))\n : RETRY_AFTER_DEFAULT_SEC;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n // v0.10.0 (M3): warn once \u2014 a persistent 429 repeats at debug.\n const line = `Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`;\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.warn(line);\n }\n break;\n }\n case \"FORBIDDEN\": {\n // v0.4.2 (P3): 403 is a permission issue (e.g. Premium subscription\n // expired). Reauth wouldn't help \u2014 surface a clear hint.\n // v0.10.0 (M3): once at error level, repeats at debug \u2014 not 144\n // identical error lines per day for one unchanged account problem.\n const line =\n \"parcel.app returned 403 Forbidden \u2014 your account may not have an active Premium subscription, or the API key was revoked. Check your account on parcelapp.net.\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"INVALID_API_KEY\": {\n // v0.10.0 (M3): first occurrence at error (the user must fix the\n // config; info.connection goes red too) \u2014 repeats at debug.\n const line = \"Invalid API key \u2014 please check your parcel.app API key\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"NETWORK\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n }\n break;\n case \"TIMEOUT\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n }\n break;\n default:\n if (isRepeat) {\n // Same error as last time \u2014 don't spam the log\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.error(`Poll failed: ${error.message}`);\n }\n }\n\n // C2: setStateChangedAsync avoids redundant `false` writes on sustained\n // failure. The `.catch` keeps poll() from rejecting when the broker is\n // already down, so the fire-and-forget callers never see an unhandled\n // rejection (no global process handler needed).\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker shutting down \u2014 ignore */\n });\n }\n}\n\n// Process entry point. Not reachable from the unit tests (they import the class\n// instead of running the file as a process), but NOT untested: `npm run\n// test:integration` boots the adapter through exactly this branch inside a real\n// js-controller, which is what catches module-scope startup crashes.\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "parcelapp",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.10.4": {
|
|
7
|
+
"en": "Malformed parcel.app replies and odd checkConnection input get a clear answer; a brief database hiccup no longer turns the connection indicator red.",
|
|
8
|
+
"de": "Fehlerhafte parcel.app-Antworten und unpassende checkConnection-Eingaben bekommen eine klare Antwort; ein kurzer Datenbank-Aussetzer färbt die Verbindungsanzeige nicht mehr rot.",
|
|
9
|
+
"ru": "Некорректные ответы parcel.app и неверный ввод в checkConnection получают понятный ответ; короткий сбой базы данных больше не делает индикатор связи красным.",
|
|
10
|
+
"pt": "Respostas malformadas do parcel.app e entradas estranhas em checkConnection recebem uma resposta clara; uma breve falha da base de dados já não põe o indicador de ligação a vermelho.",
|
|
11
|
+
"nl": "Misvormde parcel.app-antwoorden en vreemde checkConnection-invoer krijgen een duidelijk antwoord; een korte databasehik maakt de verbindingsindicator niet meer rood.",
|
|
12
|
+
"fr": "Les réponses malformées de parcel.app et les entrées inattendues de checkConnection reçoivent une réponse claire ; un bref raté de la base de données ne rend plus l'indicateur de connexion rouge.",
|
|
13
|
+
"it": "Risposte malformate di parcel.app e input anomali di checkConnection ricevono una risposta chiara; un breve inciampo del database non rende più rosso l'indicatore di connessione.",
|
|
14
|
+
"es": "Respuestas malformadas de parcel.app y entradas extrañas en checkConnection reciben una respuesta clara; un breve tropiezo de la base de datos ya no pone en rojo el indicador de conexión.",
|
|
15
|
+
"pl": "Wadliwe odpowiedzi parcel.app i nietypowe dane w checkConnection dostają jasną odpowiedź; krótka czkawka bazy danych nie zmienia już wskaźnika połączenia na czerwony.",
|
|
16
|
+
"uk": "Некоректні відповіді parcel.app і неправильний ввід у checkConnection отримують зрозумілу відповідь; короткий збій бази даних більше не робить індикатор з'єднання червоним.",
|
|
17
|
+
"zh-cn": "parcel.app 的异常回复和 checkConnection 的异常输入会得到明确回应;数据库的短暂中断不再使连接指示变红。"
|
|
18
|
+
},
|
|
19
|
+
"0.10.3": {
|
|
20
|
+
"en": "Stopping or restarting the adapter was cut short — the stopped instance kept claiming a live connection to parcel.app instead of showing as disconnected.",
|
|
21
|
+
"de": "Beenden oder Neustart des Adapters brach vorzeitig ab — die gestoppte Instanz meldete weiterhin eine bestehende Verbindung zu parcel.app statt getrennt zu sein.",
|
|
22
|
+
"ru": "Остановка или перезапуск адаптера обрывались — остановленный экземпляр продолжал сообщать о действующем соединении с parcel.app вместо разрыва.",
|
|
23
|
+
"pt": "Parar ou reiniciar o adaptador era interrompido — a instância parada continuava a indicar uma ligação ativa ao parcel.app em vez de aparecer desligada.",
|
|
24
|
+
"nl": "Stoppen of herstarten van de adapter werd afgebroken — de gestopte instantie meldde nog steeds een actieve verbinding met parcel.app in plaats van verbroken.",
|
|
25
|
+
"fr": "L'arrêt ou le redémarrage de l'adaptateur était interrompu — l'instance arrêtée indiquait toujours une connexion active à parcel.app au lieu d'être déconnectée.",
|
|
26
|
+
"it": "L'arresto o il riavvio dell'adattatore veniva interrotto — l'istanza fermata continuava a segnalare una connessione attiva a parcel.app invece di risultare disconnessa.",
|
|
27
|
+
"es": "Detener o reiniciar el adaptador se interrumpía — la instancia detenida seguía indicando una conexión activa con parcel.app en lugar de aparecer desconectada.",
|
|
28
|
+
"pl": "Zatrzymanie lub restart adaptera był przerywany — zatrzymana instancja nadal zgłaszała aktywne połączenie z parcel.app zamiast rozłączenia.",
|
|
29
|
+
"uk": "Зупинка або перезапуск адаптера обривалися — зупинений екземпляр далі повідомляв про активне з'єднання з parcel.app замість роз'єднання.",
|
|
30
|
+
"zh-cn": "停止或重启适配器时过程被中断——已停止的实例仍显示与 parcel.app 保持连接,而不是显示为已断开。"
|
|
31
|
+
},
|
|
6
32
|
"0.10.2": {
|
|
7
33
|
"en": "Internal cleanup. No user-facing changes.",
|
|
8
34
|
"de": "Interne Aufräumarbeiten. Keine Änderungen für Nutzer.",
|
|
@@ -67,32 +93,6 @@
|
|
|
67
93
|
"pl": "Okno dostawy jest teraz pokazywane także dla przewoźników, którzy podają je tylko jako zakres daty/godziny, a nie jako znacznik czasu Unix.",
|
|
68
94
|
"uk": "Вікно доставки тепер показується й для перевізників, які повідомляють його лише як діапазон дати/часу, а не як позначку часу Unix.",
|
|
69
95
|
"zh-cn": "现在,对于仅以日期/时间范围(而非 Unix 时间戳)报告的承运商,也会显示派送时段。"
|
|
70
|
-
},
|
|
71
|
-
"0.7.2": {
|
|
72
|
-
"en": "Much quieter state updates: a package's last-updated timestamp only changes when its tracking data actually changed, and device entries are no longer rewritten on every poll.",
|
|
73
|
-
"de": "Deutlich ruhigere State-Updates: der Zuletzt-aktualisiert-Zeitstempel eines Pakets ändert sich nur noch bei echten Datenänderungen, Geräte-Einträge werden nicht mehr jeden Poll neu geschrieben.",
|
|
74
|
-
"ru": "Обновления стали спокойнее: метка последнего обновления посылки меняется только при реальном изменении данных, записи устройств не перезаписываются при каждом опросе.",
|
|
75
|
-
"pt": "Atualizações muito mais calmas: o carimbo de última atualização de um pacote só muda quando os dados realmente mudam e as entradas de dispositivos não são mais reescritas a cada sondagem.",
|
|
76
|
-
"nl": "Veel rustigere updates: het laatst-bijgewerkt-tijdstempel van een pakket verandert alleen bij echte datawijzigingen en apparaatvermeldingen worden niet meer bij elke poll herschreven.",
|
|
77
|
-
"fr": "Mises à jour plus calmes : l'horodatage de dernière mise à jour d'un colis ne change que si ses données changent réellement, les entrées d'appareils ne sont plus réécrites à chaque cycle.",
|
|
78
|
-
"it": "Aggiornamenti più tranquilli: il timestamp di ultimo aggiornamento di un pacco cambia solo quando i dati cambiano davvero, le voci dei dispositivi non vengono più riscritte a ogni ciclo.",
|
|
79
|
-
"es": "Actualizaciones más tranquilas: la marca de última actualización de un paquete solo cambia cuando sus datos realmente cambian y las entradas ya no se reescriben en cada sondeo.",
|
|
80
|
-
"pl": "Spokojniejsze aktualizacje: znacznik ostatniej aktualizacji paczki zmienia się tylko przy realnej zmianie danych, wpisy urządzeń nie są przepisywane przy każdym odpytaniu.",
|
|
81
|
-
"uk": "Спокійніші оновлення: мітка останнього оновлення посилки змінюється лише за реальної зміни даних, записи пристроїв не перезаписуються щоопитування.",
|
|
82
|
-
"zh-cn": "状态更新更安静:包裹的最后更新时间戳仅在跟踪数据真正变化时才更新,设备条目不再每次轮询都被重写。"
|
|
83
|
-
},
|
|
84
|
-
"0.7.1": {
|
|
85
|
-
"en": "Fixed a timezone edge case in delivery estimates: when the API reports only a calendar date, the estimate could be off by a day in time zones west of UTC — now stable.",
|
|
86
|
-
"de": "Zeitzonen-Randfall bei Lieferschätzungen behoben: Wenn die API nur ein Kalenderdatum liefert, konnte die Schätzung in Zeitzonen westlich von UTC einen Tag danebenliegen — jetzt stabil.",
|
|
87
|
-
"ru": "Исправлен пограничный случай с часовым поясом в оценках доставки: если API возвращает только дату, оценка могла отличаться на день в зонах западнее UTC — теперь стабильно.",
|
|
88
|
-
"pt": "Corrigido um caso-limite de fuso horário nas estimativas de entrega: quando a API fornece apenas uma data, a estimativa podia falhar por um dia em fusos a oeste de UTC — agora estável.",
|
|
89
|
-
"nl": "Tijdzone-randgeval in bezorgschattingen opgelost: als de API alleen een kalenderdatum geeft, kon de schatting in tijdzones ten westen van UTC een dag afwijken — nu stabiel.",
|
|
90
|
-
"fr": "Correction d'un cas limite de fuseau horaire dans les estimations de livraison : quand l'API ne fournit qu'une date, l'estimation pouvait décaler d'un jour à l'ouest d'UTC — désormais stable.",
|
|
91
|
-
"it": "Corretto un caso limite di fuso orario nelle stime di consegna: quando l'API fornisce solo una data, la stima poteva sbagliare di un giorno nei fusi a ovest di UTC — ora stabile.",
|
|
92
|
-
"es": "Corregido un caso límite de zona horaria en las estimaciones de entrega: cuando la API solo da una fecha, la estimación podía fallar por un día en zonas al oeste de UTC — ahora estable.",
|
|
93
|
-
"pl": "Naprawiono przypadek brzegowy strefy czasowej w szacowanym czasie dostawy: gdy API podaje tylko datę, szacunek mógł różnić się o dzień w strefach na zachód od UTC — teraz stabilny.",
|
|
94
|
-
"uk": "Виправлено граничний випадок часового поясу в оцінках доставки: коли API повертає лише дату, оцінка могла відрізнятися на день у зонах на захід від UTC — тепер стабільно.",
|
|
95
|
-
"zh-cn": "修复了配送预计时间的时区边界问题:当 API 仅提供日期时,在 UTC 以西时区的预计可能相差一天,现在已稳定。"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"plugins": {
|
|
@@ -157,9 +157,6 @@
|
|
|
157
157
|
"adminUI": {
|
|
158
158
|
"config": "json"
|
|
159
159
|
},
|
|
160
|
-
"supportedMessages": {
|
|
161
|
-
"stopInstance": true
|
|
162
|
-
},
|
|
163
160
|
"dependencies": [
|
|
164
161
|
{
|
|
165
162
|
"js-controller": ">=7.2.2"
|
|
@@ -167,7 +164,7 @@
|
|
|
167
164
|
],
|
|
168
165
|
"globalDependencies": [
|
|
169
166
|
{
|
|
170
|
-
"admin": ">=
|
|
167
|
+
"admin": ">=8.0.11"
|
|
171
168
|
}
|
|
172
169
|
]
|
|
173
170
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.parcelapp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "ioBroker adapter for the parcel.app API",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "krobi",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@types/iobroker": "npm:@iobroker/types@^7.2.2",
|
|
44
44
|
"@types/node": "^22.19.17",
|
|
45
45
|
"@vitest/coverage-v8": "^4.1.11",
|
|
46
|
+
"iobroker-adapter-checks": "^0.3.0",
|
|
46
47
|
"rimraf": "^6.1.3",
|
|
47
48
|
"typescript": "~6.0.3",
|
|
48
49
|
"vitest": "^4.1.11"
|
|
@@ -68,12 +69,19 @@
|
|
|
68
69
|
"translate": "translate-adapter",
|
|
69
70
|
"lint": "eslint",
|
|
70
71
|
"lint:fix": "eslint --fix",
|
|
71
|
-
"format": "prettier --write .",
|
|
72
|
-
"format:check": "prettier --check .",
|
|
72
|
+
"format": "prettier --write . \"!build/**\" \"!io-package.json\" \"!.github/dependabot.yml\"",
|
|
73
|
+
"format:check": "prettier --check . \"!build/**\" \"!io-package.json\" \"!.github/dependabot.yml\"",
|
|
73
74
|
"release": "release-script"
|
|
74
75
|
},
|
|
75
76
|
"bugs": {
|
|
76
77
|
"url": "https://github.com/krobipd/ioBroker.parcelapp/issues"
|
|
77
78
|
},
|
|
78
|
-
"main": "build/main.js"
|
|
79
|
+
"main": "build/main.js",
|
|
80
|
+
"overrides": {
|
|
81
|
+
"mocha": {
|
|
82
|
+
"diff": "^8.0.3",
|
|
83
|
+
"serialize-javascript": "^7.0.5"
|
|
84
|
+
},
|
|
85
|
+
"esbuild": ">=0.25.0"
|
|
86
|
+
}
|
|
79
87
|
}
|