Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import app from "../../inmobile.app.mjs";
import utils from "../../common/utils.mjs";

export default {
key: "inmobile-send-sms-messages",
name: "Send SMS Messages",
description: "Send one or more SMS messages using the InMobile API. [See the documentation](https://www.inmobile.com/docs/rest-api)",
version: "0.0.1",
type: "action",
annotations: {
readOnlyHint: false,
destructiveHint: true,
openWorldHint: true,
},
props: {
app,
messages: {
type: "string[]",
label: "Messages",
description: `An array of message objects (1-250 messages). Each message object supports the following properties:
**Required Fields:**
- \`to\` (string): The msisdn (country code and number) to send to. Remember to include country code, e.g. \`4512345678\`
- \`text\` (string): The text message (1-10000 characters)
- \`from\` (string): The sender. Can be a 3-11 character text sender or up to 14 digit sender number
**Optional Fields:**
- \`countryHint\` (string): Country code for optimal phone number validation, e.g. \`44\` or \`GB\`
- \`messageId\` (string): Optional message ID to identify the message (1-50 characters, must be unique)
- \`respectBlacklist\` (boolean): Block message if target number is blacklisted. Default: \`true\`
- \`validityPeriodInSeconds\` (integer): Validity period (60-172800 seconds). Default: \`172800\` (48 hours)
- \`statusCallbackUrl\` (string): URL for delivery status callbacks
- \`sendTime\` (string): Schedule message for future sending, e.g. \`2024-12-31T14:50:23Z\` (UTC time)
- \`msisdnCooldownInMinutes\` (integer): Prevent sending to same number within period (60-43200 minutes)
- \`flash\` (boolean): Send as flash message (class0). Default: \`false\`
- \`encoding\` (string): Message encoding - \`gsm7\` (default, 160 chars), \`ucs2\` (70 chars), or \`auto\`
**Example:**
\`\`\`json
[
{
"to": "4512345678",
"text": "Hello World",
"from": "YourSender",
"encoding": "gsm7"
},
{
"to": "4587654321",
"text": "Another message",
"from": "YourSender",
"flash": true
}
]
\`\`\``,
},
},
async run({ $ }) {
const {
app,
messages,
} = this;

const response = await app.sendSms({
$,
data: {
messages: utils.parseArray(messages),
},
});

$.export("$summary", `Successfully sent ${messages.length} SMS message(s)`);
return response;
},
};
68 changes: 68 additions & 0 deletions components/inmobile/common/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const parseJson = (input, maxDepth = 100) => {
const seen = new WeakSet();
const parse = (value) => {
if (maxDepth <= 0) {
return value;
}
if (typeof(value) === "string") {
// Only parse if the string looks like a JSON object or array
const trimmed = value.trim();
if (
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"))
) {
try {
return parseJson(JSON.parse(value), maxDepth - 1);
} catch (e) {
return value;
}
}
return value;
} else if (typeof(value) === "object" && value !== null && !Array.isArray(value)) {
if (seen.has(value)) {
return value;
}
seen.add(value);
return Object.entries(value)
.reduce((acc, [
key,
val,
]) => Object.assign(acc, {
[key]: parse(val),
}), {});
} else if (Array.isArray(value)) {
return value.map((item) => parse(item));
}
return value;
};

return parse(input);
};
Comment on lines +1 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Incomplete depth limiting and cycle detection for arrays.

The parseJson function has several correctness issues:

  1. Depth not decremented for object/array traversal (lines 26-34): maxDepth is only decremented when recursively parsing JSON strings (line 15), but when traversing existing objects or arrays (lines 26-34), the depth counter is not reduced. This allows deep nesting to bypass the depth limit.

  2. Arrays not checked for cycles (line 33-34): While objects are checked against the seen WeakSet (line 22), arrays are not. This creates an infinite loop if an array contains a circular reference to itself.

  3. Inconsistent recursion (line 15): Recursive call uses parseJson(JSON.parse(value), maxDepth - 1) instead of the inner parse function, which bypasses the seen set and starts fresh cycle detection. This breaks cycle protection across recursive calls.

Apply this diff to fix depth limiting and cycle detection:

 const parseJson = (input, maxDepth = 100) => {
   const seen = new WeakSet();
-  const parse = (value) => {
+  const parse = (value, depth) => {
-    if (maxDepth <= 0) {
+    if (depth <= 0) {
       return value;
     }
     if (typeof(value) === "string") {
       // Only parse if the string looks like a JSON object or array
       const trimmed = value.trim();
       if (
         (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
         (trimmed.startsWith("[") && trimmed.endsWith("]"))
       ) {
         try {
-          return parseJson(JSON.parse(value), maxDepth - 1);
+          return parse(JSON.parse(value), depth - 1);
         } catch (e) {
           return value;
         }
       }
       return value;
     } else if (typeof(value) === "object" && value !== null && !Array.isArray(value)) {
       if (seen.has(value)) {
         return value;
       }
       seen.add(value);
       return Object.entries(value)
         .reduce((acc, [
           key,
           val,
         ]) => Object.assign(acc, {
-          [key]: parse(val),
+          [key]: parse(val, depth - 1),
         }), {});
     } else if (Array.isArray(value)) {
+      if (seen.has(value)) {
+        return value;
+      }
+      seen.add(value);
-      return value.map((item) => parse(item));
+      return value.map((item) => parse(item, depth - 1));
     }
     return value;
   };

-  return parse(input);
+  return parse(input, maxDepth);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parseJson = (input, maxDepth = 100) => {
const seen = new WeakSet();
const parse = (value) => {
if (maxDepth <= 0) {
return value;
}
if (typeof(value) === "string") {
// Only parse if the string looks like a JSON object or array
const trimmed = value.trim();
if (
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"))
) {
try {
return parseJson(JSON.parse(value), maxDepth - 1);
} catch (e) {
return value;
}
}
return value;
} else if (typeof(value) === "object" && value !== null && !Array.isArray(value)) {
if (seen.has(value)) {
return value;
}
seen.add(value);
return Object.entries(value)
.reduce((acc, [
key,
val,
]) => Object.assign(acc, {
[key]: parse(val),
}), {});
} else if (Array.isArray(value)) {
return value.map((item) => parse(item));
}
return value;
};
return parse(input);
};
const parseJson = (input, maxDepth = 100) => {
const seen = new WeakSet();
const parse = (value, depth) => {
if (depth <= 0) {
return value;
}
if (typeof(value) === "string") {
// Only parse if the string looks like a JSON object or array
const trimmed = value.trim();
if (
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"))
) {
try {
return parse(JSON.parse(value), depth - 1);
} catch (e) {
return value;
}
}
return value;
} else if (typeof(value) === "object" && value !== null && !Array.isArray(value)) {
if (seen.has(value)) {
return value;
}
seen.add(value);
return Object.entries(value)
.reduce((acc, [
key,
val,
]) => Object.assign(acc, {
[key]: parse(val, depth - 1),
}), {});
} else if (Array.isArray(value)) {
if (seen.has(value)) {
return value;
}
seen.add(value);
return value.map((item) => parse(item, depth - 1));
}
return value;
};
return parse(input, maxDepth);
};
🤖 Prompt for AI Agents
In components/inmobile/common/utils.mjs around lines 1–40, the recursive parse
logic fails to decrement maxDepth for object/array traversal, doesn't detect
cycles for arrays, and calls parseJson (resetting the seen set) when parsing
JSON strings; change the inner parse to accept a currentDepth parameter (or
otherwise decrement maxDepth on recursive calls), use parse(parsedValue,
currentDepth - 1) instead of parseJson(...) when parsing strings, and when
handling arrays check seen.has(value) and seen.add(value) just like for objects
before mapping; ensure every recursive call into objects/arrays passes a
decremented depth and reuses the same seen WeakSet so depth limiting and cycle
detection work consistently.


function parseArray(input, maxDepth = 100) {
if (typeof input === "string") {
const trimmed = input.trim();
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) {
return parsed.map((item) => parseArray(item, maxDepth - 1));
}
} catch (e) {
throw new Error(`Invalid JSON array format: ${e.message}`);
}
}
return parseJson(input, maxDepth);
}

if (Array.isArray(input)) {
return input.map((item) => parseArray(item, maxDepth));
}

return input;
}
Comment on lines +42 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Depth limit not enforced; inconsistent error handling.

The parseArray function has correctness issues:

  1. No depth decrement (lines 49, 59): Recursive calls to parseArray don't decrement maxDepth, allowing unbounded recursion and stack overflow.

  2. Inconsistent error handling (lines 45-55): If a string looks like an array but JSON.parse fails (line 52), an error is thrown. However, if the string doesn't look like an array at all, it falls back to parseJson (line 55) which silently returns the input on parse errors. This creates unpredictable behavior: "[invalid" throws, but "invalid" returns silently.

Apply this diff to fix depth limiting and error handling:

 function parseArray(input, maxDepth = 100) {
+  if (maxDepth <= 0) {
+    return input;
+  }
+
   if (typeof input === "string") {
     const trimmed = input.trim();
     if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
       try {
         const parsed = JSON.parse(trimmed);
         if (Array.isArray(parsed)) {
           return parsed.map((item) => parseArray(item, maxDepth - 1));
         }
       } catch (e) {
         throw new Error(`Invalid JSON array format: ${e.message}`);
       }
     }
     return parseJson(input, maxDepth);
   }

   if (Array.isArray(input)) {
     return input.map((item) => parseArray(item, maxDepth - 1));
   }

   return input;
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In components/inmobile/common/utils.mjs around lines 42 to 63, enforce the
maxDepth limit and make error handling consistent: at the top of parseArray
return/throw when maxDepth <= 0 (e.g., throw a "max recursion depth exceeded"
error), decrement maxDepth on every recursive call (pass maxDepth - 1 when
calling parseArray for items and when delegating to parseJson), and stop
throwing on failed JSON.parse of a string-looking-array; instead, on JSON.parse
catch, fallback to calling parseJson(input, maxDepth - 1) so behavior matches
non-bracketed strings.


export default {
parseJson,
parseArray,
};
35 changes: 31 additions & 4 deletions components/inmobile/inmobile.app.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
import { axios } from "@pipedream/platform";

export default {
type: "app",
app: "inmobile",
propDefinitions: {},
methods: {
// this.$auth contains connected account data
authKeys() {
console.log(Object.keys(this.$auth));
getUrl(path) {
return `https://api.inmobile.com/v4${path}`;
},
getAuth() {
return {
username: "x",
password: this.$auth.api_key,
};
},
makeRequest({
$ = this, path, ...args
}) {
return axios($, {
url: this.getUrl(path),
auth: this.getAuth(),
...args,
});
},
post(args = {}) {
return this.makeRequest({
method: "POST",
...args,
});
},
sendSms(args = {}) {
return this.post({
path: "/sms/outgoing",
...args,
});
},
},
};
5 changes: 4 additions & 1 deletion components/inmobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/inmobile",
"version": "0.0.1",
"version": "0.1.0",
"description": "Pipedream InMobile Components",
"main": "inmobile.app.mjs",
"keywords": [
Expand All @@ -11,5 +11,8 @@
"author": "Pipedream <[email protected]> (https://pipedream.com/)",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@pipedream/platform": "^3.1.0"
}
}
Loading
Loading