@@ -15,15 +15,33 @@ export function convertAmzTimeToMs(timestamp: string) {
1515}
1616
1717/**
18- * Convert UTC timestamp to ISO 8601 timestamp
19- * @param timestamp of UTC form: Fri, 10 Feb 2012 21:34:55 GMT
20- * @return ISO8601 timestamp of form: YYYYMMDDTHHMMSSZ
21- */
22- export function convertUTCtoISO8601 ( timestamp : string | number ) {
23- // convert to ISO string: YYYY-MM-DDTHH:mm:ss.sssZ.
24- const converted = new Date ( timestamp ) . toISOString ( ) ;
25- // Remove "-"s and "."s and milliseconds
26- return converted . split ( '.' ) [ 0 ] . replace ( / - | : / g, '' ) . concat ( 'Z' ) ;
18+ * Convert UTC timestamp to ISO 8601 compact format
19+ * @param timestamp - UTC timestamp (e.g., 'Fri, 10 Feb 2012 21:34:55 GMT') or Unix timestamp
20+ * @return ISO8601 timestamp of form YYYYMMDDTHHMMSSZ, or undefined if invalid
21+ *
22+ * @example
23+ * convertUTCtoISO8601('Fri, 10 Feb 2012 21:34:55 GMT'); // '20120210T213455Z'
24+ * convertUTCtoISO8601(1328910895000); // '20120210T213455Z'
25+ * convertUTCtoISO8601('invalid'); // undefined
26+ */
27+ export function convertUTCtoISO8601 ( timestamp : unknown ) : string | undefined {
28+ if ( timestamp == null ) {
29+ return undefined ;
30+ }
31+
32+ const date = new Date ( timestamp as string | number ) ;
33+
34+ if ( isNaN ( date . getTime ( ) ) ) {
35+ return undefined ;
36+ }
37+
38+ try {
39+ // Can throw RangeError.
40+ const converted = date . toISOString ( ) ;
41+ return converted . split ( '.' ) [ 0 ] . replace ( / - | : / g, '' ) . concat ( 'Z' ) ;
42+ } catch {
43+ return undefined ;
44+ }
2745}
2846
2947/**
@@ -41,16 +59,85 @@ export function checkTimeSkew(timestamp: string, expiry: number, log: RequestLog
4159 if ( ( currentTime + fifteenMinutes ) < parsedTimestamp ) {
4260 log . debug ( 'current time pre-dates timestamp' , {
4361 parsedTimestamp,
44- currentTimeInMilliseconds : currentTime } ) ;
62+ currentTimeInMilliseconds : currentTime
63+ } ) ;
4564 return true ;
4665 }
4766 const expiryInMilliseconds = expiry * 1000 ;
4867 if ( currentTime > parsedTimestamp + expiryInMilliseconds ) {
4968 log . debug ( 'signature has expired' , {
5069 parsedTimestamp,
5170 expiry,
52- currentTimeInMilliseconds : currentTime } ) ;
71+ currentTimeInMilliseconds : currentTime
72+ } ) ;
5373 return true ;
5474 }
5575 return false ;
5676}
77+
78+ /**
79+ * Validates if a string is in ISO 8601 compact format: YYYYMMDDTHHMMSSZ
80+ *
81+ * Checks that:
82+ * - String is exactly 16 characters long
83+ * - Format matches YYYYMMDDTHHMMSSZ (8 digits, 'T', 6 digits, 'Z')
84+ * - All date/time components are valid (no Feb 30th, no 25:00:00, etc.)
85+ * - No silent date corrections occur (prevents rollover)
86+ *
87+ * @param str - The string to validate
88+ * @returns true if the string is a valid ISO 8601 compact format, false otherwise
89+ *
90+ * @example
91+ * ```typescript
92+ * isValidISO8601Compact('20160208T201405Z'); // true
93+ * isValidISO8601Compact('20160230T201405Z'); // false (Feb 30 invalid)
94+ * isValidISO8601Compact('20160208T251405Z'); // false (25 hours invalid)
95+ * isValidISO8601Compact('2016-02-08T20:14:05Z'); // false (wrong format)
96+ * isValidISO8601Compact('abcd0208T201405Z'); // false (contains letters)
97+ * ```
98+ */
99+ export function isValidISO8601Compact ( str : string ) : boolean {
100+ if ( str == null || typeof str !== 'string' ) {
101+ return false ;
102+ }
103+
104+ // Check length
105+ if ( str . length !== 16 ) {
106+ return false ;
107+ }
108+
109+ // Check 'T' at position 8 and 'Z' at position 15
110+ if ( str [ 8 ] !== 'T' || str [ 15 ] !== 'Z' ) {
111+ return false ;
112+ }
113+
114+ // Check all other positions are digits
115+ for ( let i = 0 ; i < 16 ; i ++ ) {
116+ if ( i === 8 || i === 15 ) {
117+ continue ; // Skip T and Z
118+ }
119+ if ( str [ i ] < '0' || str [ i ] > '9' ) {
120+ return false ;
121+ }
122+ }
123+
124+ // Extract components
125+ const year = str . substring ( 0 , 4 ) ;
126+ const month = str . substring ( 4 , 6 ) ;
127+ const day = str . substring ( 6 , 8 ) ;
128+ const hour = str . substring ( 9 , 11 ) ;
129+ const minute = str . substring ( 11 , 13 ) ;
130+ const second = str . substring ( 13 , 15 ) ;
131+
132+ // Construct standard ISO format and validate
133+ const isoString = `${ year } -${ month } -${ day } T${ hour } :${ minute } :${ second } .000Z` ;
134+ const date = new Date ( isoString ) ;
135+
136+ try {
137+ // date.toISOString() can throw.
138+ // date.toISOString() === isoString check prevents silent date corrections (30 February to 1 March)
139+ return ! isNaN ( date . getTime ( ) ) && date . toISOString ( ) === isoString ;
140+ } catch {
141+ return false ;
142+ }
143+ }
0 commit comments