-
-
Save drudge/01f204a6fe30e3f2477bf7834297414f to your computer and use it in GitHub Desktop.
NetSuite SuiteScript to queue messages on an AWS SQS queue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @NModuleScope Public | |
* @NApiVersion 2.1 | |
*/ | |
function ExampleSQSUsage(log, runtime, sqs) { | |
const awsAccessKeyId = '...'; // TODO: Pull from a secure storage area | |
const awsSecretKey = '...'; // TODO: Pull from a secure storage area | |
const queueName = runtime.envType == runtime.EnvType.PRODUCTION | |
? '000000000000/order-processing-production' | |
: '000000000000/order-processing-sandbox'; | |
try { | |
const messageId = sqs.sendMessage({ | |
awsAccessKeyId, | |
awsSecretKey, | |
awsRegion: 'us-east-1', | |
queueName, | |
body: JSON.stringify([{ internalid: 1234, quantity: 1 }]), | |
}); | |
// TODO: Do something with `messageId` | |
} catch (error) { | |
// TODO: `Failed to send notification: ${error.message}` | |
} | |
} | |
define([ 'N/log', 'N/runtime', './sqs' ], ExampleSQSUsage); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function CryptoShim() { | |
/* | |
CryptoJS v3.1.2 | |
code.google.com/p/crypto-js | |
(c) 2009-2013 by Jeff Mott. All rights reserved. | |
code.google.com/p/crypto-js/wiki/License | |
*/ | |
var CryptoJS = CryptoJS || function (h, s) { | |
var f = {}, g = f.lib = {}, q = function () { }, m = g.Base = { extend: function (a) { q.prototype = this; var c = new q; a && c.mixIn(a); c.hasOwnProperty("init") || (c.init = function () { c.$super.init.apply(this, arguments) }); c.init.prototype = c; c.$super = this; return c }, create: function () { var a = this.extend(); a.init.apply(a, arguments); return a }, init: function () { }, mixIn: function (a) { for (var c in a) a.hasOwnProperty(c) && (this[c] = a[c]); a.hasOwnProperty("toString") && (this.toString = a.toString) }, clone: function () { return this.init.prototype.extend(this) } }, | |
r = g.WordArray = m.extend({ | |
init: function (a, c) { a = this.words = a || []; this.sigBytes = c != s ? c : 4 * a.length }, toString: function (a) { return (a || k).stringify(this) }, concat: function (a) { var c = this.words, d = a.words, b = this.sigBytes; a = a.sigBytes; this.clamp(); if (b % 4) for (var e = 0; e < a; e++)c[b + e >>> 2] |= (d[e >>> 2] >>> 24 - 8 * (e % 4) & 255) << 24 - 8 * ((b + e) % 4); else if (65535 < d.length) for (e = 0; e < a; e += 4)c[b + e >>> 2] = d[e >>> 2]; else c.push.apply(c, d); this.sigBytes += a; return this }, clamp: function () { | |
var a = this.words, c = this.sigBytes; a[c >>> 2] &= 4294967295 << | |
32 - 8 * (c % 4); a.length = h.ceil(c / 4) | |
}, clone: function () { var a = m.clone.call(this); a.words = this.words.slice(0); return a }, random: function (a) { for (var c = [], d = 0; d < a; d += 4)c.push(4294967296 * h.random() | 0); return new r.init(c, a) } | |
}), l = f.enc = {}, k = l.Hex = { | |
stringify: function (a) { var c = a.words; a = a.sigBytes; for (var d = [], b = 0; b < a; b++) { var e = c[b >>> 2] >>> 24 - 8 * (b % 4) & 255; d.push((e >>> 4).toString(16)); d.push((e & 15).toString(16)) } return d.join("") }, parse: function (a) { | |
for (var c = a.length, d = [], b = 0; b < c; b += 2)d[b >>> 3] |= parseInt(a.substr(b, | |
2), 16) << 24 - 4 * (b % 8); return new r.init(d, c / 2) | |
} | |
}, n = l.Latin1 = { stringify: function (a) { var c = a.words; a = a.sigBytes; for (var d = [], b = 0; b < a; b++)d.push(String.fromCharCode(c[b >>> 2] >>> 24 - 8 * (b % 4) & 255)); return d.join("") }, parse: function (a) { for (var c = a.length, d = [], b = 0; b < c; b++)d[b >>> 2] |= (a.charCodeAt(b) & 255) << 24 - 8 * (b % 4); return new r.init(d, c) } }, j = l.Utf8 = { stringify: function (a) { try { return decodeURIComponent(escape(n.stringify(a))) } catch (c) { throw Error("Malformed UTF-8 data"); } }, parse: function (a) { return n.parse(unescape(encodeURIComponent(a))) } }, | |
u = g.BufferedBlockAlgorithm = m.extend({ | |
reset: function () { this._data = new r.init; this._nDataBytes = 0 }, _append: function (a) { "string" == typeof a && (a = j.parse(a)); this._data.concat(a); this._nDataBytes += a.sigBytes }, _process: function (a) { var c = this._data, d = c.words, b = c.sigBytes, e = this.blockSize, f = b / (4 * e), f = a ? h.ceil(f) : h.max((f | 0) - this._minBufferSize, 0); a = f * e; b = h.min(4 * a, b); if (a) { for (var g = 0; g < a; g += e)this._doProcessBlock(d, g); g = d.splice(0, a); c.sigBytes -= b } return new r.init(g, b) }, clone: function () { | |
var a = m.clone.call(this); | |
a._data = this._data.clone(); return a | |
}, _minBufferSize: 0 | |
}); g.Hasher = u.extend({ | |
cfg: m.extend(), init: function (a) { this.cfg = this.cfg.extend(a); this.reset() }, reset: function () { u.reset.call(this); this._doReset() }, update: function (a) { this._append(a); this._process(); return this }, finalize: function (a) { a && this._append(a); return this._doFinalize() }, blockSize: 16, _createHelper: function (a) { return function (c, d) { return (new a.init(d)).finalize(c) } }, _createHmacHelper: function (a) { | |
return function (c, d) { | |
return (new t.HMAC.init(a, | |
d)).finalize(c) | |
} | |
} | |
}); var t = f.algo = {}; return f | |
}(Math); | |
(function (h) { | |
for (var s = CryptoJS, f = s.lib, g = f.WordArray, q = f.Hasher, f = s.algo, m = [], r = [], l = function (a) { return 4294967296 * (a - (a | 0)) | 0 }, k = 2, n = 0; 64 > n;) { var j; a: { j = k; for (var u = h.sqrt(j), t = 2; t <= u; t++)if (!(j % t)) { j = !1; break a } j = !0 } j && (8 > n && (m[n] = l(h.pow(k, 0.5))), r[n] = l(h.pow(k, 1 / 3)), n++); k++ } var a = [], f = f.SHA256 = q.extend({ | |
_doReset: function () { this._hash = new g.init(m.slice(0)) }, _doProcessBlock: function (c, d) { | |
for (var b = this._hash.words, e = b[0], f = b[1], g = b[2], j = b[3], h = b[4], m = b[5], n = b[6], q = b[7], p = 0; 64 > p; p++) { | |
if (16 > p) a[p] = | |
c[d + p] | 0; else { var k = a[p - 15], l = a[p - 2]; a[p] = ((k << 25 | k >>> 7) ^ (k << 14 | k >>> 18) ^ k >>> 3) + a[p - 7] + ((l << 15 | l >>> 17) ^ (l << 13 | l >>> 19) ^ l >>> 10) + a[p - 16] } k = q + ((h << 26 | h >>> 6) ^ (h << 21 | h >>> 11) ^ (h << 7 | h >>> 25)) + (h & m ^ ~h & n) + r[p] + a[p]; l = ((e << 30 | e >>> 2) ^ (e << 19 | e >>> 13) ^ (e << 10 | e >>> 22)) + (e & f ^ e & g ^ f & g); q = n; n = m; m = h; h = j + k | 0; j = g; g = f; f = e; e = k + l | 0 | |
} b[0] = b[0] + e | 0; b[1] = b[1] + f | 0; b[2] = b[2] + g | 0; b[3] = b[3] + j | 0; b[4] = b[4] + h | 0; b[5] = b[5] + m | 0; b[6] = b[6] + n | 0; b[7] = b[7] + q | 0 | |
}, _doFinalize: function () { | |
var a = this._data, d = a.words, b = 8 * this._nDataBytes, e = 8 * a.sigBytes; | |
d[e >>> 5] |= 128 << 24 - e % 32; d[(e + 64 >>> 9 << 4) + 14] = h.floor(b / 4294967296); d[(e + 64 >>> 9 << 4) + 15] = b; a.sigBytes = 4 * d.length; this._process(); return this._hash | |
}, clone: function () { var a = q.clone.call(this); a._hash = this._hash.clone(); return a } | |
}); s.SHA256 = q._createHelper(f); s.HmacSHA256 = q._createHmacHelper(f) | |
})(Math); | |
(function () { | |
var h = CryptoJS, s = h.enc.Utf8; h.algo.HMAC = h.lib.Base.extend({ | |
init: function (f, g) { f = this._hasher = new f.init; "string" == typeof g && (g = s.parse(g)); var h = f.blockSize, m = 4 * h; g.sigBytes > m && (g = f.finalize(g)); g.clamp(); for (var r = this._oKey = g.clone(), l = this._iKey = g.clone(), k = r.words, n = l.words, j = 0; j < h; j++)k[j] ^= 1549556828, n[j] ^= 909522486; r.sigBytes = l.sigBytes = m; this.reset() }, reset: function () { var f = this._hasher; f.reset(); f.update(this._iKey) }, update: function (f) { this._hasher.update(f); return this }, finalize: function (f) { | |
var g = | |
this._hasher; f = g.finalize(f); g.reset(); return g.finalize(this._oKey.clone().concat(f)) | |
} | |
}) | |
})(); | |
/* | |
CryptoJS v3.1.2 | |
code.google.com/p/crypto-js | |
(c) 2009-2013 by Jeff Mott. All rights reserved. | |
code.google.com/p/crypto-js/wiki/License | |
*/ | |
(function () { | |
var h = CryptoJS, j = h.lib.WordArray; h.enc.Base64 = { | |
stringify: function (b) { | |
var e = b.words, f = b.sigBytes, c = this._map; b.clamp(); b = []; for (var a = 0; a < f; a += 3) { | |
for (var d = (e[a >>> 2] >>> 24 - 8 * (a % 4) & 255) << 16 | (e[a + 1 >>> 2] >>> 24 - 8 * ((a + 1) % 4) & 255) << 8 | e[a + 2 >>> 2] >>> 24 - 8 * ((a + 2) % 4) & 255, g = 0; 4 > g && a + 0.75 * g < f; g++) { b.push(c.charAt(d >>> 6 * (3 - g) & 63)); } | |
} if (e = c.charAt(64)) { | |
for (; b.length % 4;) { b.push(e); } | |
} return b.join("") | |
}, parse: function (b) { | |
var e = b.length, f = this._map, c = f.charAt(64); c && (c = b.indexOf(c), -1 != c && (e = c)); for (var c = [], a = 0, d = 0; d < | |
e; d++) { | |
if (d % 4) { var g = f.indexOf(b.charAt(d - 1)) << 2 * (d % 4), h = f.indexOf(b.charAt(d)) >>> 6 - 2 * (d % 4); c[a >>> 2] |= (g | h) << 24 - 8 * (a % 4); a++ } | |
} return j.create(c, a) | |
}, _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" | |
} | |
})(); | |
return CryptoJS; | |
} | |
define([], CryptoShim); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Copyright 2021 Nicholas Penree <npenree@fye.com> | |
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | |
documentation files (the "Software"), to deal in the Software without restriction, including without limitation | |
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and | |
to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all copies or substantial portions of | |
the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | |
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | |
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | |
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* sqs.js | |
* | |
* Copyright (c) Nicholas Penree. All rights reserved. | |
* Licensed under the MIT license. See LICENSE for details. | |
* | |
* @author Nicholas Penree <npenree@fye.com> | |
* @NModuleScope Public | |
* @NApiVersion 2.1 | |
*/ | |
function SQS(crypto, https, log, CryptoJS) { | |
const SERVICE_NAME_SQS = 'sqs'; | |
function getAmzDate() { | |
const amzDate = `${new Date().toISOString().split('.')[0]}Z`; | |
return amzDate.replace(/-/g, '').replace(/:/g, ''); | |
} | |
function rfc3986EncodeURIComponent(str) { | |
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16)}`); | |
} | |
function getSignatureKey(key, dateStamp, regionName, serviceName) { | |
const kDate = CryptoJS.HmacSHA256(dateStamp, `AWS4${key}`); | |
const kRegion = CryptoJS.HmacSHA256(regionName, kDate); | |
const kService = CryptoJS.HmacSHA256(serviceName, kRegion); | |
return CryptoJS.HmacSHA256('aws4_request', kService); | |
} | |
/** | |
* Sends a message to an AWS SQS queue. | |
* | |
* Options: | |
* @param awsAccessKeyId string | |
* @param awsSecretKey string | |
* @param awsRegion string defaults to us-east-1 | |
* @param queueName string | |
* @param body? string | |
* @param attributes? array<Attribute> | |
* | |
* Attributes: | |
* @param name string | |
* @param value string | |
* @param type string defaults to String | |
* | |
* @api public | |
* @returns string|null SQS message id if successful | |
* @param options object | |
*/ | |
function sendMessage({ | |
awsAccessKeyId: accessKey, | |
awsSecretKey: secretKey, | |
awsRegion: region = 'us-east-1', | |
queueName, | |
attributes = [], | |
body = '', | |
host = SERVICE_NAME_SQS + '.' + region + '.amazonaws.com', | |
LOGGER_TAG = 'sqs.sendMessage', | |
}) { | |
const amzDate = getAmzDate(); | |
const datestamp = amzDate.split("T")[0]; | |
log.debug(LOGGER_TAG, `amz date=${amzDate}`); | |
let params = "Action=SendMessage"; | |
attributes.forEach((attrib, index) => { | |
const pos = index + 1; | |
if (attrib.name) { | |
params += `&MessageAttribute.${pos}.Name=${encodeURIComponent(attrib.name)}`; | |
} | |
params += `&MessageAttribute.${pos}.Value.DataType=${encodeURIComponent(attrib.type || 'String')}`; | |
if (attrib.value) { | |
params += `&MessageAttribute.${pos}.Value.StringValue=${encodeURIComponent(attrib.value)}`; | |
} | |
}); | |
params += `&MessageBody=${rfc3986EncodeURIComponent(body)}&Version=2012-11-05`; | |
// ************* TASK 1: CREATE A CANONICAL REQUEST ************* | |
const canonicalUri = `/${queueName}`; | |
const canonicalQuerystring = params; | |
const canonicalHeaders = 'host:' + host + '\n' + 'x-amz-date:' + amzDate + '\n'; | |
const signedHeaders = 'host;x-amz-date'; | |
const payloadHash = crypto.createHash({ algorithm: crypto.HashAlg.SHA256 }); | |
const canonicalRequest = 'GET\n' + | |
canonicalUri + '\n' + | |
canonicalQuerystring + '\n' + | |
canonicalHeaders + '\n' + | |
signedHeaders + '\n' + | |
payloadHash.digest().toLowerCase(); | |
log.debug(LOGGER_TAG, `canonical request=${JSON.stringify(canonicalRequest)}`); | |
// ************* TASK 2: CREATE THE STRING TO SIGN************* | |
const algorithm = 'AWS4-HMAC-SHA256'; | |
const credentialScope = datestamp + '/' + region + '/' + SERVICE_NAME_SQS + '/' + 'aws4_request'; | |
const canonicalHash = crypto.createHash({ algorithm: crypto.HashAlg.SHA256 }); | |
canonicalHash.update({ input: canonicalRequest }); | |
const stringToSign = algorithm + '\n' + | |
amzDate + '\n' + | |
credentialScope + '\n' + | |
canonicalHash.digest().toLowerCase(); | |
log.debug(LOGGER_TAG, 'string to sign=' + stringToSign); | |
// ************* TASK 3: CALCULATE THE SIGNATURE ************* | |
log.debug(LOGGER_TAG, 'signing key=' + secretKey + "\n" + datestamp + "\n" + region + "\n" + SERVICE_NAME_SQS); | |
const signingKey = getSignatureKey(secretKey, datestamp, region, SERVICE_NAME_SQS); | |
const signature = CryptoJS.HmacSHA256(stringToSign, signingKey); | |
const authorizationHeader = algorithm + ' ' + | |
'Credential=' + accessKey + '/' + credentialScope + ', ' + | |
'SignedHeaders=' + signedHeaders + ', ' + 'Signature=' + signature; | |
const response = https.get({ | |
url: `https://${host}${canonicalUri}?${canonicalQuerystring}`, | |
headers: { | |
'host': host, | |
'x-amz-date': amzDate, | |
'Authorization': authorizationHeader | |
}, | |
}); | |
log.debug(LOGGER_TAG, `response=${JSON.stringify(response)}`); | |
if (response.code == 200) { | |
const matches = /\<MessageId\>(.*)\<\/MessageId\>/.exec(response.body); | |
if (matches && matches.length > 1) { | |
const messageId = matches[1]; | |
log.debug(LOGGER_TAG, `Sent Message to SQS messageId=${messageId}`); | |
return messageId; | |
} | |
} | |
log.error(LOGGER_TAG, `Sent Message Failed: ${response.body}`); | |
return null; | |
} | |
/** | |
* Exports `sendMessage`. | |
*/ | |
return { | |
sendMessage, | |
}; | |
} | |
define([ | |
'N/crypto', | |
'N/https', | |
'N/log', | |
'./crypto', | |
], SQS); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment