conduyt 1.69.0 → 1.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,6 +131,8 @@ out and named in `_omitted`. Every HTTP refusal takes this path (JSON calls, tex
131
131
  Error: Conduyt API 503: Contact merge in progress [Retry-After: 5s] {"code":"merge_in_progress"}
132
132
  ```
133
133
 
134
+ The contact merge route (`POST /contacts/merge`, no command today) answers a retryable 503 with `Retry-After` and the code `merge_contacts_busy` (a contact stayed busy for the whole lock budget) or `merge_budget_exhausted` (the request's time budget ran out), nothing changed. A command that reaches it prints the refusal in this grammar, exits 1 and does not retry; run it again after the stated seconds.
135
+
134
136
  ## License
135
137
 
136
138
  MIT
package/dist/client.js CHANGED
@@ -83,6 +83,58 @@ export function renderErrorDetails(body) {
83
83
  out._omitted = omitted;
84
84
  return JSON.stringify(out);
85
85
  }
86
+ const RA_MAX_SECONDS = 2_147_483_647; // delay-seconds is unbounded 1*DIGIT: a longer value saturates here instead of becoming null
87
+ const RA_MONTHS = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
88
+ const RA_MON = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
89
+ const RA_IMF = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d{2}) (${RA_MON}) (\\d{4}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
90
+ const RA_850 = new RegExp(`^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\\d{2})-(${RA_MON})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
91
+ const RA_ASC = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (${RA_MON}) ([ \\d]\\d) (\\d{2}):(\\d{2}):(\\d{2}) (\\d{4})$`);
92
+ /** A UTC timestamp for the parts, or NaN when any part is out of range (31 Feb, 25:00, ...). */
93
+ function raUtc(y, mo, d, h, mi, s) {
94
+ if (y < 1900 || h > 23 || mi > 59 || s > 60)
95
+ return NaN; // RFC 5322: years below 1900 are not dates
96
+ const t = Date.UTC(y, mo, d, h, mi, Math.min(s, 59));
97
+ const back = new Date(t);
98
+ return back.getUTCFullYear() === y && back.getUTCMonth() === mo && back.getUTCDate() === d ? t : NaN;
99
+ }
100
+ /**
101
+ * The ONE Retry-After parser. RFC 9110: delay-seconds (digits; a value too long for a number saturates at 2^31-1) or an
102
+ * HTTP-date in one of the three §5.6.7 forms, parsed explicitly and always as UTC (never the host timezone): IMF-fixdate,
103
+ * RFC 850 (two-digit year: more than 50 years ahead of `nowMs` means the previous century) and asctime. A date becomes the
104
+ * remaining seconds, floored at 0. Absent, blank or anything else (ISO timestamps, other zones, "-3") is null.
105
+ */
106
+ export function parseRetryAfter(raw, nowMs = Date.now()) {
107
+ if (raw === null || raw === undefined)
108
+ return null;
109
+ const v = raw.trim();
110
+ if (v === "")
111
+ return null;
112
+ if (/^\d+(\.\d+)?$/.test(v)) {
113
+ const n = Number(v);
114
+ return Number.isNaN(n) ? null : Math.min(n, RA_MAX_SECONDS);
115
+ }
116
+ let t = NaN;
117
+ let m;
118
+ if ((m = RA_IMF.exec(v)))
119
+ t = raUtc(Number(m[3]), RA_MONTHS[m[2]], Number(m[1]), Number(m[4]), Number(m[5]), Number(m[6]));
120
+ else if ((m = RA_850.exec(v))) {
121
+ const century = Math.floor(new Date(nowMs).getUTCFullYear() / 100) * 100;
122
+ const mo = RA_MONTHS[m[2]], d = Number(m[1]), h = Number(m[4]), mi = Number(m[5]), sec = Number(m[6]);
123
+ t = raUtc(century + Number(m[3]), mo, d, h, mi, sec);
124
+ // RFC 9110: a timestamp more than 50 years in the future (the same instant 50 calendar years on, UTC) is the previous century
125
+ // (the day is clamped to the month's last day: 29 Feb + 50 years is 28 Feb, never 1 Mar)
126
+ const n = new Date(nowMs);
127
+ const ly = n.getUTCFullYear() + 50, lm = n.getUTCMonth();
128
+ const limit = Date.UTC(ly, lm, Math.min(n.getUTCDate(), new Date(Date.UTC(ly, lm + 1, 0)).getUTCDate()), n.getUTCHours(), n.getUTCMinutes(), n.getUTCSeconds(), n.getUTCMilliseconds());
129
+ if (!Number.isNaN(t) && t > limit)
130
+ t = raUtc(century - 100 + Number(m[3]), mo, d, h, mi, sec);
131
+ }
132
+ else if ((m = RA_ASC.exec(v)))
133
+ t = raUtc(Number(m[6]), RA_MONTHS[m[1]], Number(m[2].trim()), Number(m[3]), Number(m[4]), Number(m[5]));
134
+ if (Number.isNaN(t))
135
+ return null;
136
+ return Math.max(0, Math.ceil((t - nowMs) / 1000));
137
+ }
86
138
  /**
87
139
  * The ONE place an HTTP refusal becomes an Error (request, postText and stream all use it). Grammar (stable):
88
140
  * `Conduyt API <status>: <message>[ [Retry-After: <N>s]][ <details>]`; <details> is ONE JSON object and the FINAL token
@@ -90,9 +142,8 @@ export function renderErrorDetails(body) {
90
142
  */
91
143
  export function formatHttpError(res, text) {
92
144
  const json = text ? safeParse(text) : null;
93
- const ra = res.headers.get("retry-after");
94
- const raN = ra === null || ra.trim() === "" ? NaN : Number(ra);
95
- const raTag = Number.isFinite(raN) && raN >= 0 ? ` [Retry-After: ${raN}s]` : "";
145
+ const raN = parseRetryAfter(res.headers.get("retry-after"));
146
+ const raTag = raN !== null ? ` [Retry-After: ${raN}s]` : "";
96
147
  const obj = json && typeof json === "object" && !Array.isArray(json) ? json : null;
97
148
  if (obj && typeof obj.error === "string" && obj.error) {
98
149
  const extras = renderErrorDetails(obj);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.69.0",
3
+ "version": "1.70.0",
4
4
  "description": "Command-line interface for Conduyt CRM — manage contacts, deals, pipelines, and run insight queries from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",