@instantkom/cli 3.169.0 → 3.170.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.
@@ -42,6 +42,11 @@ export class ApiClient {
42
42
  if (!response.ok)
43
43
  return false;
44
44
  const data = await this.parseResponse(response);
45
+ // A 2xx without an accessToken (empty/malformed body) must not count as a
46
+ // successful refresh -- otherwise ensureJwtToken hands back a stale/undefined
47
+ // token and the next request sends `Authorization: Bearer undefined` (#5264c).
48
+ if (!data.accessToken)
49
+ return false;
45
50
  this.jwtAccessToken = data.accessToken;
46
51
  if (data.refreshToken)
47
52
  this.jwtRefreshToken = data.refreshToken;
@@ -51,9 +51,7 @@ export default class Send extends BaseCommand {
51
51
  }
52
52
  const scheduledAt = parseSendAt(flags['send-at']);
53
53
  const media = prepareMediaFiles(flags.media);
54
- const idempotencyHeaders = flags['idempotency-key']
55
- ? { 'Idempotency-Key': flags['idempotency-key'] }
56
- : undefined;
54
+ const idempotencyKey = flags['idempotency-key'];
57
55
  if (flags['dry-run']) {
58
56
  const payload = {
59
57
  channelId: flags.channel,
@@ -77,9 +75,22 @@ export default class Send extends BaseCommand {
77
75
  const channelAttempts = flags.channels
78
76
  ? await resolveFallbackChannels(client, flags.channels)
79
77
  : [{ alias: String(flags.channel), channelId: flags.channel }];
78
+ // Only the multi-channel fallback needs per-channel idempotency keys.
79
+ // Reusing one key across fallback channels let the API dedupe the SMS retry
80
+ // against the failed WhatsApp attempt, silently defeating fallback (#5264b).
81
+ // A single explicit channel keeps the exact key the caller passed, so
82
+ // audit-by-key / retry-with-key behaviour is unchanged.
83
+ const isFallback = channelAttempts.length > 1;
80
84
  const failures = [];
81
85
  for (const attempt of channelAttempts) {
82
86
  try {
87
+ const idempotencyHeaders = idempotencyKey
88
+ ? {
89
+ 'Idempotency-Key': isFallback
90
+ ? `${idempotencyKey}-${attempt.channelId}`
91
+ : idempotencyKey,
92
+ }
93
+ : undefined;
83
94
  const message = await this.sendViaChannel({
84
95
  channelId: attempt.channelId,
85
96
  client,
@@ -1,8 +1,13 @@
1
1
  import { ApiError } from '../errors/api-error.js';
2
- const ALIASES = {
3
- wa: ['whatsapp', 'wa'],
4
- whatsapp: ['whatsapp', 'wa'],
5
- sms: ['sms'],
2
+ // Messenger aliases resolve to numeric gateway type ids (channel.gatewayTypeId,
3
+ // = the DB gwtype). Matching on the id is robust against the API returning human
4
+ // display names for gatewayType ("WhatsApp Cloud API"), which an exact string
5
+ // match would miss. wa/whatsapp cover both the legacy on-prem WhatsApp (13) and
6
+ // the modern Cloud API (17). Keep in sync with GatewayType (services/api).
7
+ const ALIAS_GATEWAY_TYPE_IDS = {
8
+ wa: [13, 17],
9
+ whatsapp: [13, 17],
10
+ sms: [2],
6
11
  };
7
12
  function channelsFromResponse(response) {
8
13
  if (Array.isArray(response))
@@ -10,13 +15,31 @@ function channelsFromResponse(response) {
10
15
  return response.data ?? response.items ?? [];
11
16
  }
12
17
  function matchesAlias(channel, alias) {
13
- const needles = ALIASES[alias.toLowerCase()] ?? [alias.toLowerCase()];
14
- const haystack = [
15
- String(channel.id),
16
- channel.name ?? '',
17
- channel.gatewayType ?? '',
18
- ].join(' ').toLowerCase();
19
- return needles.some((needle) => haystack.includes(needle));
18
+ const normalized = alias.toLowerCase();
19
+ // A numeric alias addresses a channel by its exact id. Substring matching
20
+ // here let '2' false-match ids 20/12/21 (#5264a).
21
+ if (/^\d+$/.test(normalized)) {
22
+ return String(channel.id) === normalized;
23
+ }
24
+ const gatewayType = (channel.gatewayType ?? '').toLowerCase();
25
+ // Known messenger aliases match on the numeric gateway type id, so multi-word
26
+ // display names ("WhatsApp Cloud API", gatewayTypeId 17) resolve correctly.
27
+ // The user-chosen channel name is deliberately never consulted -- a channel
28
+ // named "WA Backup via SMS" must not receive a WhatsApp message (#5264a).
29
+ const typeIds = ALIAS_GATEWAY_TYPE_IDS[normalized];
30
+ if (typeIds) {
31
+ if (channel.gatewayTypeId !== undefined) {
32
+ return typeIds.includes(channel.gatewayTypeId);
33
+ }
34
+ // Defensive fallback if a response omits gatewayTypeId: match the gateway
35
+ // display name by prefix (still never the channel name).
36
+ return normalized === 'sms'
37
+ ? gatewayType === 'sms'
38
+ : gatewayType.startsWith('whatsapp');
39
+ }
40
+ // Arbitrary alias: exact case-insensitive match on the gateway display name
41
+ // (never a substring, never the channel name).
42
+ return gatewayType === normalized;
20
43
  }
21
44
  export async function resolveFallbackChannels(client, aliasesCsv) {
22
45
  const aliases = aliasesCsv
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@instantkom/cli",
3
- "version": "3.169.0",
3
+ "version": "3.170.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@instantkom/cli",
9
- "version": "3.169.0",
9
+ "version": "3.170.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@oclif/core": "^4",