diff --git a/.changeset/odd-pumpkins-poke.md b/.changeset/odd-pumpkins-poke.md new file mode 100644 index 000000000..e122592c1 --- /dev/null +++ b/.changeset/odd-pumpkins-poke.md @@ -0,0 +1,6 @@ +--- +"@logto/connector-aliyun-dm": patch +"@logto/connector-aliyun-sms": patch +--- + +Fix Aliyun Direct Mail and Aliyun Short Message Service connectors' signature functions. diff --git a/packages/connectors/connector-aliyun-dm/src/utils.ts b/packages/connectors/connector-aliyun-dm/src/utils.ts index 895539f10..59791fb8d 100644 --- a/packages/connectors/connector-aliyun-dm/src/utils.ts +++ b/packages/connectors/connector-aliyun-dm/src/utils.ts @@ -1,5 +1,6 @@ +import { type Optional } from '@silverhand/essentials'; import { got } from 'got'; -import { createHmac } from 'node:crypto'; +import { createHmac, randomUUID } from 'node:crypto'; import type { PublicParameters } from './types.js'; @@ -7,26 +8,29 @@ import type { PublicParameters } from './types.js'; // https://help.aliyun.com/document_detail/29442.html const escaper = (string_: string) => encodeURIComponent(string_) - .replace(/\*/g, '%2A') - .replace(/'/g, '%27') .replace(/!/g, '%21') .replace(/"/g, '%22') + .replace(/'/g, '%27') .replace(/\(/g, '%28') .replace(/\)/g, '%29') + .replace(/\*/g, '%2A') .replace(/\+/g, '%2B'); +// Format date string to 'YYYY-MM-DDThh:mm:ssZ' format. +const formatDateString = (date: Date) => { + const rawString = date.toISOString(); + return rawString.replace(/\.\d{3}Z$/, 'Z'); // Trim milliseconds. +}; + export const getSignature = ( parameters: Record, secret: string, method: string ) => { - const canonicalizedQuery = Object.keys(parameters) - .map((key) => { - const value = parameters[key]; - - return value === undefined ? '' : `${escaper(key)}=${escaper(value)}`; + const canonicalizedQuery = Object.entries(parameters) + .map(([key, value]) => { + return `${escaper(key)}=${escaper(value)}`; }) - .filter(Boolean) .slice() .sort() .join('&'); @@ -41,11 +45,14 @@ export const request = async ( parameters: PublicParameters & Record, accessKeySecret: string ) => { - const finalParameters: Record = { + const finalParameters = Object.entries>({ ...parameters, - SignatureNonce: String(Math.random()), - Timestamp: new Date().toISOString(), - }; + SignatureNonce: randomUUID(), + Timestamp: formatDateString(new Date()), + }).reduce>( + (result, [key, value]) => (value === undefined ? result : { ...result, [key]: value }), + {} + ); const signature = getSignature(finalParameters, accessKeySecret, 'POST'); return got.post({ diff --git a/packages/connectors/connector-aliyun-sms/src/utils.ts b/packages/connectors/connector-aliyun-sms/src/utils.ts index 895539f10..0c4e1e45e 100644 --- a/packages/connectors/connector-aliyun-sms/src/utils.ts +++ b/packages/connectors/connector-aliyun-sms/src/utils.ts @@ -1,32 +1,35 @@ +import { type Optional } from '@silverhand/essentials'; import { got } from 'got'; -import { createHmac } from 'node:crypto'; +import { createHmac, randomUUID } from 'node:crypto'; import type { PublicParameters } from './types.js'; - // Aliyun has special escape rules. // https://help.aliyun.com/document_detail/29442.html const escaper = (string_: string) => encodeURIComponent(string_) - .replace(/\*/g, '%2A') - .replace(/'/g, '%27') .replace(/!/g, '%21') .replace(/"/g, '%22') + .replace(/'/g, '%27') .replace(/\(/g, '%28') .replace(/\)/g, '%29') + .replace(/\*/g, '%2A') .replace(/\+/g, '%2B'); +// Format date string to 'YYYY-MM-DDThh:mm:ssZ' format. +const formatDateString = (date: Date) => { + const rawString = date.toISOString(); + return rawString.replace(/\.\d{3}Z$/, 'Z'); // Trim milliseconds. +}; + export const getSignature = ( parameters: Record, secret: string, method: string ) => { - const canonicalizedQuery = Object.keys(parameters) - .map((key) => { - const value = parameters[key]; - - return value === undefined ? '' : `${escaper(key)}=${escaper(value)}`; + const canonicalizedQuery = Object.entries(parameters) + .map(([key, value]) => { + return `${escaper(key)}=${escaper(value)}`; }) - .filter(Boolean) .slice() .sort() .join('&'); @@ -41,11 +44,14 @@ export const request = async ( parameters: PublicParameters & Record, accessKeySecret: string ) => { - const finalParameters: Record = { + const finalParameters = Object.entries>({ ...parameters, - SignatureNonce: String(Math.random()), - Timestamp: new Date().toISOString(), - }; + SignatureNonce: randomUUID(), + Timestamp: formatDateString(new Date()), + }).reduce>( + (result, [key, value]) => (value === undefined ? result : { ...result, [key]: value }), + {} + ); const signature = getSignature(finalParameters, accessKeySecret, 'POST'); return got.post({