@transferwise/components 0.0.0-experimental-fa3aa98 → 0.0.0-experimental-f4a328f

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.
Files changed (201) hide show
  1. package/build/Calendar/Calendar.js +16 -94
  2. package/build/Calendar/Calendar.js.map +1 -1
  3. package/build/Calendar/Calendar.messages.js +0 -12
  4. package/build/Calendar/Calendar.messages.js.map +1 -1
  5. package/build/Calendar/Calendar.messages.mjs +0 -12
  6. package/build/Calendar/Calendar.messages.mjs.map +1 -1
  7. package/build/Calendar/Calendar.mjs +17 -95
  8. package/build/Calendar/Calendar.mjs.map +1 -1
  9. package/build/Calendar/DateHeader/DateHeader.js +1 -5
  10. package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
  11. package/build/Calendar/DateHeader/DateHeader.mjs +1 -5
  12. package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
  13. package/build/Calendar/DayCalendar/DayCalendar.js +0 -14
  14. package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
  15. package/build/Calendar/DayCalendar/DayCalendar.mjs +0 -14
  16. package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
  17. package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +19 -53
  18. package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
  19. package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +19 -53
  20. package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
  21. package/build/Calendar/MonthCalendar/MonthCalendar.js +0 -6
  22. package/build/Calendar/MonthCalendar/MonthCalendar.js.map +1 -1
  23. package/build/Calendar/MonthCalendar/MonthCalendar.mjs +0 -6
  24. package/build/Calendar/MonthCalendar/MonthCalendar.mjs.map +1 -1
  25. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +3 -8
  26. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
  27. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +3 -8
  28. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
  29. package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +3 -8
  30. package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
  31. package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +3 -8
  32. package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
  33. package/build/Calendar/YearCalendar/YearCalendar.js +0 -7
  34. package/build/Calendar/YearCalendar/YearCalendar.js.map +1 -1
  35. package/build/Calendar/YearCalendar/YearCalendar.mjs +0 -7
  36. package/build/Calendar/YearCalendar/YearCalendar.mjs.map +1 -1
  37. package/build/Calendar/{TableLink → tableLink}/TableLink.js +11 -26
  38. package/build/Calendar/tableLink/TableLink.js.map +1 -0
  39. package/build/Calendar/{TableLink → tableLink}/TableLink.mjs +11 -26
  40. package/build/Calendar/tableLink/TableLink.mjs.map +1 -0
  41. package/build/DateLookup/DateLookup.js +9 -20
  42. package/build/DateLookup/DateLookup.js.map +1 -1
  43. package/build/DateLookup/DateLookup.mjs +9 -20
  44. package/build/DateLookup/DateLookup.mjs.map +1 -1
  45. package/build/Markup/Markup.js +137 -0
  46. package/build/Markup/Markup.js.map +1 -0
  47. package/build/Markup/Markup.mjs +132 -0
  48. package/build/Markup/Markup.mjs.map +1 -0
  49. package/build/Markup/constants.js +36 -0
  50. package/build/Markup/constants.js.map +1 -0
  51. package/build/Markup/constants.mjs +29 -0
  52. package/build/Markup/constants.mjs.map +1 -0
  53. package/build/Markup/utils/parseMarkup.js +236 -0
  54. package/build/Markup/utils/parseMarkup.js.map +1 -0
  55. package/build/Markup/utils/parseMarkup.mjs +231 -0
  56. package/build/Markup/utils/parseMarkup.mjs.map +1 -0
  57. package/build/Markup/utils/sanitise.js +114 -0
  58. package/build/Markup/utils/sanitise.js.map +1 -0
  59. package/build/Markup/utils/sanitise.mjs +111 -0
  60. package/build/Markup/utils/sanitise.mjs.map +1 -0
  61. package/build/i18n/en.json +0 -4
  62. package/build/i18n/en.json.js +0 -4
  63. package/build/i18n/en.json.js.map +1 -1
  64. package/build/i18n/en.json.mjs +0 -4
  65. package/build/i18n/en.json.mjs.map +1 -1
  66. package/build/index.js +4 -2
  67. package/build/index.js.map +1 -1
  68. package/build/index.mjs +2 -1
  69. package/build/index.mjs.map +1 -1
  70. package/build/main.css +78 -123
  71. package/build/styles/Calendar/Calendar.css +37 -112
  72. package/build/styles/Markup/Markup.css +28 -0
  73. package/build/styles/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
  74. package/build/styles/main.css +78 -123
  75. package/build/types/Body/Body.d.ts +2 -2
  76. package/build/types/Calendar/Calendar.d.ts +3 -16
  77. package/build/types/Calendar/Calendar.d.ts.map +1 -1
  78. package/build/types/Calendar/Calendar.messages.d.ts +0 -20
  79. package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
  80. package/build/types/Calendar/DateHeader/DateHeader.d.ts +0 -2
  81. package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
  82. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +1 -5
  83. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
  84. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +0 -4
  85. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
  86. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +0 -1
  87. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
  88. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +1 -2
  89. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
  90. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +1 -2
  91. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
  92. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +0 -1
  93. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
  94. package/build/types/Calendar/index.d.ts +0 -1
  95. package/build/types/Calendar/index.d.ts.map +1 -1
  96. package/build/types/Calendar/{TableLink → tableLink}/TableLink.d.ts +1 -3
  97. package/build/types/Calendar/tableLink/TableLink.d.ts.map +1 -0
  98. package/build/types/Calendar/tableLink/index.d.ts.map +1 -0
  99. package/build/types/DateLookup/DateLookup.d.ts +5 -24
  100. package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
  101. package/build/types/DateLookup/index.d.ts +0 -1
  102. package/build/types/DateLookup/index.d.ts.map +1 -1
  103. package/build/types/IconButton/IconButton.d.ts +1 -1
  104. package/build/types/Markup/Markup.d.ts +18 -0
  105. package/build/types/Markup/Markup.d.ts.map +1 -0
  106. package/build/types/Markup/Markup.types.d.ts +77 -0
  107. package/build/types/Markup/Markup.types.d.ts.map +1 -0
  108. package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts +12 -0
  109. package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts.map +1 -0
  110. package/build/types/Markup/constants.d.ts +20 -0
  111. package/build/types/Markup/constants.d.ts.map +1 -0
  112. package/build/types/Markup/index.d.ts +3 -0
  113. package/build/types/Markup/index.d.ts.map +1 -0
  114. package/build/types/Markup/utils/parseMarkup.d.ts +8 -0
  115. package/build/types/Markup/utils/parseMarkup.d.ts.map +1 -0
  116. package/build/types/Markup/utils/sanitise.d.ts +4 -0
  117. package/build/types/Markup/utils/sanitise.d.ts.map +1 -0
  118. package/build/types/Title/Title.d.ts +2 -2
  119. package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
  120. package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
  121. package/build/types/common/DateUtils/index.d.ts +0 -2
  122. package/build/types/common/DateUtils/index.d.ts.map +1 -1
  123. package/build/types/index.d.ts +4 -3
  124. package/build/types/index.d.ts.map +1 -1
  125. package/build/types/test-utils/index.d.ts +0 -8
  126. package/build/types/test-utils/index.d.ts.map +1 -1
  127. package/package.json +1 -1
  128. package/src/Calendar/Calendar.css +37 -112
  129. package/src/Calendar/Calendar.less +35 -5
  130. package/src/Calendar/Calendar.messages.ts +0 -21
  131. package/src/Calendar/Calendar.test.tsx +27 -288
  132. package/src/Calendar/Calendar.tsx +19 -136
  133. package/src/Calendar/DateHeader/DateHeader.tsx +0 -6
  134. package/src/Calendar/DayCalendar/DayCalendar.tsx +1 -33
  135. package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +28 -62
  136. package/src/Calendar/MonthCalendar/MonthCalendar.tsx +0 -8
  137. package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +3 -15
  138. package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +3 -15
  139. package/src/Calendar/YearCalendar/YearCalendar.tsx +0 -9
  140. package/src/Calendar/_stories/Calendar.story.tsx +4 -138
  141. package/src/Calendar/index.ts +0 -7
  142. package/src/Calendar/{TableLink → tableLink}/TableLink.tsx +14 -29
  143. package/src/DateLookup/DateLookup.test.tsx +37 -34
  144. package/src/DateLookup/DateLookup.tsx +13 -37
  145. package/src/DateLookup/_stories/DateLookup.story.tsx +2 -124
  146. package/src/DateLookup/index.ts +0 -6
  147. package/src/Markup/Markup.css +28 -0
  148. package/src/Markup/Markup.injection.test.tsx +163 -0
  149. package/src/Markup/Markup.less +29 -0
  150. package/src/Markup/Markup.test.tsx +463 -0
  151. package/src/Markup/Markup.tsx +169 -0
  152. package/src/Markup/Markup.types.ts +62 -0
  153. package/src/Markup/_stories/Markup.docs.mdx +75 -0
  154. package/src/Markup/_stories/Markup.security.docs.mdx +343 -0
  155. package/src/Markup/_stories/Markup.story.tsx +266 -0
  156. package/src/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
  157. package/src/Markup/_stories/Vulnerability/Vulnerability.tsx +30 -0
  158. package/src/Markup/constants.ts +41 -0
  159. package/src/Markup/index.ts +2 -0
  160. package/src/Markup/utils/parseMarkup.test.ts +706 -0
  161. package/src/Markup/utils/parseMarkup.ts +234 -0
  162. package/src/Markup/utils/sanitise.test.ts +497 -0
  163. package/src/Markup/utils/sanitise.ts +133 -0
  164. package/src/common/DateUtils/index.ts +0 -2
  165. package/src/i18n/en.json +0 -4
  166. package/src/index.ts +4 -3
  167. package/src/main.css +78 -123
  168. package/src/main.less +1 -0
  169. package/build/Calendar/TableLink/TableLink.js.map +0 -1
  170. package/build/Calendar/TableLink/TableLink.mjs.map +0 -1
  171. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +0 -33
  172. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +0 -1
  173. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +0 -31
  174. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +0 -1
  175. package/build/common/DateUtils/isDateDisabled/isDateDisabled.js +0 -40
  176. package/build/common/DateUtils/isDateDisabled/isDateDisabled.js.map +0 -1
  177. package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs +0 -38
  178. package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs.map +0 -1
  179. package/build/styles/Calendar/TableLink/TableLink.css +0 -115
  180. package/build/types/Calendar/Calendar.types.d.ts +0 -32
  181. package/build/types/Calendar/Calendar.types.d.ts.map +0 -1
  182. package/build/types/Calendar/TableLink/TableLink.d.ts.map +0 -1
  183. package/build/types/Calendar/TableLink/index.d.ts.map +0 -1
  184. package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +0 -3
  185. package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +0 -1
  186. package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts +0 -3
  187. package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts.map +0 -1
  188. package/src/Calendar/Calendar.disabledDates.test.tsx +0 -518
  189. package/src/Calendar/Calendar.types.ts +0 -35
  190. package/src/Calendar/TableLink/TableLink.css +0 -115
  191. package/src/Calendar/TableLink/TableLink.less +0 -68
  192. package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +0 -118
  193. package/src/Calendar/_stories/Calendar.test.story.tsx +0 -74
  194. package/src/DateLookup/DateLookup.disabledDates.test.tsx +0 -293
  195. package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +0 -108
  196. package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +0 -98
  197. package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +0 -45
  198. package/src/common/DateUtils/isDateDisabled/isDateDisabled.test.ts +0 -169
  199. package/src/common/DateUtils/isDateDisabled/isDateDisabled.ts +0 -59
  200. /package/build/types/Calendar/{TableLink → tableLink}/index.d.ts +0 -0
  201. /package/src/Calendar/{TableLink → tableLink}/index.ts +0 -0
@@ -0,0 +1,497 @@
1
+ import { isAllowedHref, isHrefAllowedByMode } from './sanitise';
2
+
3
+ describe('isAllowedHref', () => {
4
+ describe('allowed protocols', () => {
5
+ it('allows https URLs', () => {
6
+ expect(isAllowedHref('https://example.com')).toBe(true);
7
+ });
8
+
9
+ it('allows mailto URLs', () => {
10
+ expect(isAllowedHref('mailto:user@example.com')).toBe(true);
11
+ });
12
+
13
+ it('blocks http URLs', () => {
14
+ expect(isAllowedHref('http://example.com')).toBe(false);
15
+ });
16
+
17
+ it('blocks tel URLs', () => {
18
+ expect(isAllowedHref('tel:+1234567890')).toBe(false);
19
+ });
20
+ });
21
+
22
+ describe('allowed relative paths', () => {
23
+ it('allows root-relative paths', () => {
24
+ expect(isAllowedHref('/settings')).toBe(true);
25
+ });
26
+
27
+ it('allows hash fragments', () => {
28
+ expect(isAllowedHref('#section')).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe('blocked relative paths', () => {
33
+ it('blocks dot-relative paths', () => {
34
+ expect(isAllowedHref('./page')).toBe(false);
35
+ });
36
+
37
+ it('blocks parent-relative paths', () => {
38
+ expect(isAllowedHref('../page')).toBe(false);
39
+ });
40
+
41
+ it('blocks dot-relative paths with query parameters', () => {
42
+ expect(isAllowedHref('./page?time=10:30')).toBe(false);
43
+ });
44
+
45
+ it('blocks bare relative paths with colons in segments', () => {
46
+ expect(isAllowedHref('page?key=value:other')).toBe(false);
47
+ });
48
+ });
49
+
50
+ describe('blocked protocols', () => {
51
+ it('blocks javascript: protocol', () => {
52
+ // eslint-disable-next-line no-script-url
53
+ expect(isAllowedHref('javascript:alert(1)')).toBe(false);
54
+ });
55
+
56
+ it('blocks javascript: with mixed case', () => {
57
+ // eslint-disable-next-line no-script-url
58
+ expect(isAllowedHref('JavaScript:alert(1)')).toBe(false);
59
+ });
60
+
61
+ it('blocks data: protocol', () => {
62
+ expect(isAllowedHref('data:text/html,<script>alert(1)</script>')).toBe(false);
63
+ });
64
+
65
+ it('blocks vbscript: protocol', () => {
66
+ expect(isAllowedHref('vbscript:MsgBox("xss")')).toBe(false);
67
+ });
68
+
69
+ it('blocks unknown protocols', () => {
70
+ expect(isAllowedHref('ftp://evil.com')).toBe(false);
71
+ });
72
+ });
73
+
74
+ describe('protocol-relative URLs', () => {
75
+ it('blocks protocol-relative URLs', () => {
76
+ expect(isAllowedHref('//evil.com/phishing')).toBe(false);
77
+ });
78
+
79
+ it('blocks protocol-relative URLs with whitespace', () => {
80
+ expect(isAllowedHref(' //evil.com ')).toBe(false);
81
+ });
82
+ });
83
+
84
+ describe('edge cases', () => {
85
+ it('handles empty string', () => {
86
+ expect(isAllowedHref('')).toBe(false);
87
+ });
88
+
89
+ it('handles whitespace-padded URLs', () => {
90
+ expect(isAllowedHref(' https://example.com ')).toBe(true);
91
+ });
92
+
93
+ it('handles whitespace-padded dangerous URLs', () => {
94
+ expect(isAllowedHref(' javascript:alert(1) ')).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe('control characters in scheme — XSS bypass via URL parser throw', () => {
99
+ it('blocks null byte mid-scheme (java\\x00script:)', () => {
100
+ expect(isAllowedHref('java\x00script:alert(1)')).toBe(false);
101
+ });
102
+
103
+ it('blocks null byte between s and c (javas\\x00cript:)', () => {
104
+ expect(isAllowedHref('javas\x00cript:alert(1)')).toBe(false);
105
+ });
106
+
107
+ it('blocks SOH prefix before javascript:', () => {
108
+ expect(isAllowedHref('\x01javascript:alert(1)')).toBe(false);
109
+ });
110
+
111
+ it('blocks vertical tab prefix before javascript:', () => {
112
+ expect(isAllowedHref('\x0Bjavascript:alert(1)')).toBe(false);
113
+ });
114
+
115
+ it('blocks form feed prefix before javascript:', () => {
116
+ expect(isAllowedHref('\x0Cjavascript:alert(1)')).toBe(false);
117
+ });
118
+
119
+ it('blocks tab mid-scheme (java\\tscript:)', () => {
120
+ expect(isAllowedHref('java\tscript:alert(1)')).toBe(false);
121
+ });
122
+
123
+ it('blocks CRLF mid-scheme (java\\r\\nscript:)', () => {
124
+ expect(isAllowedHref('java\r\nscript:alert(1)')).toBe(false);
125
+ });
126
+
127
+ it('blocks LF mid-scheme (j\\navascript:)', () => {
128
+ expect(isAllowedHref('j\navascript:alert(1)')).toBe(false);
129
+ });
130
+ });
131
+
132
+ describe('zero-width and invisible character bypass', () => {
133
+ it('blocks zero-width space prefix before javascript:', () => {
134
+ expect(isAllowedHref('​javascript:alert(1)')).toBe(false);
135
+ });
136
+
137
+ it('blocks zero-width space only (invisible link)', () => {
138
+ expect(isAllowedHref('​')).toBe(false);
139
+ });
140
+
141
+ it('blocks BOM-only string (invisible link)', () => {
142
+ expect(isAllowedHref('')).toBe(false);
143
+ });
144
+
145
+ it('blocks BOM prefix before javascript:', () => {
146
+ expect(isAllowedHref('javascript:alert(1)')).toBe(false);
147
+ });
148
+
149
+ it('blocks zero-width non-joiner prefix before javascript:', () => {
150
+ expect(isAllowedHref('‌javascript:alert(1)')).toBe(false);
151
+ });
152
+
153
+ it('blocks zero-width joiner prefix before javascript:', () => {
154
+ expect(isAllowedHref('‍javascript:alert(1)')).toBe(false);
155
+ });
156
+ });
157
+
158
+ describe('non-ASCII whitespace prefix bypass', () => {
159
+ it('blocks NBSP prefix before javascript:', () => {
160
+ expect(isAllowedHref(' javascript:alert(1)')).toBe(false);
161
+ });
162
+
163
+ it('blocks line separator prefix before javascript:', () => {
164
+ expect(isAllowedHref('\u2028javascript:alert(1)')).toBe(false);
165
+ });
166
+
167
+ it('blocks paragraph separator prefix before javascript:', () => {
168
+ expect(isAllowedHref('\u2029javascript:alert(1)')).toBe(false);
169
+ });
170
+
171
+ it('blocks NBSP before protocol-relative URL', () => {
172
+ expect(isAllowedHref(' //evil.com')).toBe(false);
173
+ });
174
+
175
+ it('blocks NBSP-only string', () => {
176
+ expect(isAllowedHref(' ')).toBe(false);
177
+ });
178
+ });
179
+
180
+ describe('backslash-based open redirect', () => {
181
+ it('blocks /\\evil.com (browsers normalize \\ to /)', () => {
182
+ expect(isAllowedHref('/\\evil.com')).toBe(false);
183
+ });
184
+
185
+ it('blocks /\\\\evil.com (double backslash)', () => {
186
+ expect(isAllowedHref('/\\\\evil.com')).toBe(false);
187
+ });
188
+
189
+ it('allows /path\\with\\backslash (not at position 1)', () => {
190
+ expect(isAllowedHref('/path\\with\\backslash')).toBe(true);
191
+ });
192
+ });
193
+
194
+ describe('URL-encoded dangerous schemes', () => {
195
+ it('blocks javascript with URL-encoded colon (%3A)', () => {
196
+ expect(isAllowedHref('javascript%3Aalert(1)')).toBe(false);
197
+ });
198
+
199
+ it('blocks javascript with lowercase URL-encoded colon (%3a)', () => {
200
+ expect(isAllowedHref('javascript%3aalert(1)')).toBe(false);
201
+ });
202
+ });
203
+
204
+ describe('credential confusion / phishing via userinfo', () => {
205
+ it('blocks https URLs with misleading userinfo (good.com@evil.com)', () => {
206
+ expect(isAllowedHref('https://good.com@evil.com')).toBe(false);
207
+ });
208
+
209
+ it('blocks https URLs with port-like credentials', () => {
210
+ expect(isAllowedHref('https://evil.com:443@good.com')).toBe(false);
211
+ });
212
+
213
+ it('allows https URLs without userinfo', () => {
214
+ expect(isAllowedHref('https://example.com/path')).toBe(true);
215
+ });
216
+
217
+ it('allows mailto with @ (expected in email addresses)', () => {
218
+ expect(isAllowedHref('mailto:user@example.com')).toBe(true);
219
+ });
220
+ });
221
+
222
+ describe('degenerate HTTPS URLs', () => {
223
+ it('blocks https:evil.com (no slashes — ambiguous host)', () => {
224
+ expect(isAllowedHref('https:evil.com')).toBe(false);
225
+ });
226
+
227
+ it('blocks https:/evil.com (single slash — ambiguous)', () => {
228
+ expect(isAllowedHref('https:/evil.com')).toBe(false);
229
+ });
230
+
231
+ it('blocks https:// (no host)', () => {
232
+ expect(isAllowedHref('https://')).toBe(false);
233
+ });
234
+
235
+ it('blocks https:/// (empty host)', () => {
236
+ expect(isAllowedHref('https:///')).toBe(false);
237
+ });
238
+ });
239
+
240
+ describe('internal network / SSRF via allowed HTTPS', () => {
241
+ it('blocks https://127.0.0.1 (localhost)', () => {
242
+ expect(isAllowedHref('https://127.0.0.1/')).toBe(false);
243
+ });
244
+
245
+ it('blocks https://[::1] (IPv6 localhost)', () => {
246
+ expect(isAllowedHref('https://[::1]/')).toBe(false);
247
+ });
248
+
249
+ it('blocks hex-encoded IP (0x7f000001)', () => {
250
+ expect(isAllowedHref('https://0x7f000001/')).toBe(false);
251
+ });
252
+
253
+ it('blocks decimal IP (2130706433)', () => {
254
+ expect(isAllowedHref('https://2130706433/')).toBe(false);
255
+ });
256
+
257
+ it('blocks octal IP (0177.0.0.1)', () => {
258
+ expect(isAllowedHref('https://0177.0.0.1/')).toBe(false);
259
+ });
260
+
261
+ it('blocks AWS metadata endpoint', () => {
262
+ expect(isAllowedHref('https://169.254.169.254/')).toBe(false);
263
+ });
264
+
265
+ it('blocks GCP metadata endpoint', () => {
266
+ expect(isAllowedHref('https://metadata.google.internal/')).toBe(false);
267
+ });
268
+
269
+ it('blocks localhost with trailing dot', () => {
270
+ expect(isAllowedHref('https://localhost./')).toBe(false);
271
+ });
272
+
273
+ it('blocks .internal host with trailing dot', () => {
274
+ expect(isAllowedHref('https://metadata.google.internal./')).toBe(false);
275
+ });
276
+
277
+ it('blocks fully expanded IPv6 loopback', () => {
278
+ expect(isAllowedHref('https://[0:0:0:0:0:0:0:1]/')).toBe(false);
279
+ });
280
+ });
281
+
282
+ describe('XSS payloads in fragment and path', () => {
283
+ it('blocks XSS payload in hash fragment', () => {
284
+ expect(isAllowedHref('#<img onerror=alert(1)>')).toBe(false);
285
+ });
286
+
287
+ it('blocks XSS payload in path', () => {
288
+ expect(isAllowedHref('/<img onerror=alert(1)>')).toBe(false);
289
+ });
290
+
291
+ it('blocks attribute injection in hash fragment', () => {
292
+ expect(isAllowedHref('#" onclick="alert(1)')).toBe(false);
293
+ });
294
+
295
+ it('allows normal hash fragments', () => {
296
+ expect(isAllowedHref('#section-1')).toBe(true);
297
+ });
298
+
299
+ it('allows normal root-relative paths', () => {
300
+ expect(isAllowedHref('/normal/path?query=value')).toBe(true);
301
+ });
302
+ });
303
+
304
+ describe('UNC paths (Windows network shares)', () => {
305
+ it('blocks UNC path to external server', () => {
306
+ expect(isAllowedHref('\\\\evil.com\\share')).toBe(false);
307
+ });
308
+ });
309
+
310
+ describe('space before colon in scheme', () => {
311
+ it('blocks "javascript :alert(1)" (space before colon)', () => {
312
+ expect(isAllowedHref('javascript :alert(1)')).toBe(false);
313
+ });
314
+ });
315
+
316
+ describe('mailto abuse', () => {
317
+ it('blocks mailto with XSS in body parameter', () => {
318
+ expect(isAllowedHref('mailto:evil@evil.com?body=<script>alert(1)</script>')).toBe(false);
319
+ });
320
+
321
+ it('blocks mailto with header injection (cc/bcc)', () => {
322
+ expect(isAllowedHref('mailto:evil@evil.com?cc=victim@good.com&bcc=spy@evil.com')).toBe(false);
323
+ });
324
+
325
+ it('blocks mailto with no recipient (social engineering)', () => {
326
+ expect(isAllowedHref('mailto:?subject=urgent&body=click%20here')).toBe(false);
327
+ });
328
+
329
+ it('allows simple mailto with just an address', () => {
330
+ expect(isAllowedHref('mailto:support@example.com')).toBe(true);
331
+ });
332
+ });
333
+
334
+ describe('additional blocked protocols (regression)', () => {
335
+ it('blocks file: protocol', () => {
336
+ expect(isAllowedHref('file:///etc/passwd')).toBe(false);
337
+ });
338
+
339
+ it('blocks ws: protocol', () => {
340
+ expect(isAllowedHref('ws://evil.com')).toBe(false);
341
+ });
342
+
343
+ it('blocks wss: protocol', () => {
344
+ expect(isAllowedHref('wss://evil.com')).toBe(false);
345
+ });
346
+
347
+ it('blocks blob: protocol', () => {
348
+ expect(isAllowedHref('blob:https://evil.com/uuid')).toBe(false);
349
+ });
350
+
351
+ it('blocks data: with SVG XSS', () => {
352
+ expect(isAllowedHref('data:image/svg+xml,<svg onload=alert(1)>')).toBe(false);
353
+ });
354
+
355
+ it('blocks data: with base64-encoded script', () => {
356
+ expect(isAllowedHref('data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==')).toBe(
357
+ false,
358
+ );
359
+ });
360
+
361
+ it('blocks view-source: protocol', () => {
362
+ expect(isAllowedHref('view-source:https://evil.com')).toBe(false);
363
+ });
364
+
365
+ it('blocks custom protocol (foo:bar)', () => {
366
+ expect(isAllowedHref('foo:bar')).toBe(false);
367
+ });
368
+
369
+ it('blocks minimal protocol pattern (a:b)', () => {
370
+ expect(isAllowedHref('a:b')).toBe(false);
371
+ });
372
+
373
+ it('blocks customprotocol:payload', () => {
374
+ expect(isAllowedHref('customprotocol:payload')).toBe(false);
375
+ });
376
+ });
377
+
378
+ describe('whitespace-only inputs', () => {
379
+ it('blocks spaces only', () => {
380
+ expect(isAllowedHref(' ')).toBe(false);
381
+ });
382
+
383
+ it('blocks tab/newline/CR only', () => {
384
+ expect(isAllowedHref('\t\n\r')).toBe(false);
385
+ });
386
+ });
387
+ });
388
+
389
+ describe('isHrefAllowedByMode', () => {
390
+ describe('internal mode', () => {
391
+ it('allows root-relative paths', () => {
392
+ expect(isHrefAllowedByMode('/accounts', 'internal')).toBe(true);
393
+ });
394
+
395
+ it('allows deep root-relative paths', () => {
396
+ expect(isHrefAllowedByMode('/accounts/123/details', 'internal')).toBe(true);
397
+ });
398
+
399
+ it('allows wise.com URLs', () => {
400
+ expect(isHrefAllowedByMode('https://wise.com/help', 'internal')).toBe(true);
401
+ });
402
+
403
+ it('allows wise.com deep paths', () => {
404
+ expect(isHrefAllowedByMode('https://wise.com/gb/send-money', 'internal')).toBe(true);
405
+ });
406
+
407
+ it('blocks external domains', () => {
408
+ expect(isHrefAllowedByMode('https://example.com', 'internal')).toBe(false);
409
+ });
410
+
411
+ it('blocks domains with wise.com as substring', () => {
412
+ expect(isHrefAllowedByMode('https://wise.company.com', 'internal')).toBe(false);
413
+ });
414
+
415
+ it('blocks hash fragments', () => {
416
+ expect(isHrefAllowedByMode('#section', 'internal')).toBe(false);
417
+ });
418
+
419
+ it('blocks mailto links', () => {
420
+ expect(isHrefAllowedByMode('mailto:user@wise.com', 'internal')).toBe(false);
421
+ });
422
+
423
+ describe('URL-parser normalization bypass prevention', () => {
424
+ it('blocks /\\evil.com (backslash normalised to slash by browsers)', () => {
425
+ expect(isHrefAllowedByMode('/\\evil.com', 'internal')).toBe(false);
426
+ });
427
+
428
+ it('blocks /\\\\evil.com (double backslash)', () => {
429
+ expect(isHrefAllowedByMode('/\\\\evil.com', 'internal')).toBe(false);
430
+ });
431
+
432
+ it('allows /%2f/evil.com (percent-encoded slash stays as path segment)', () => {
433
+ expect(isHrefAllowedByMode('/%2f/evil.com', 'internal')).toBe(true);
434
+ });
435
+
436
+ it('allows normal path with encoded characters', () => {
437
+ expect(isHrefAllowedByMode('/path/%E2%9C%93/done', 'internal')).toBe(true);
438
+ });
439
+
440
+ it('blocks path that resolves to a different origin via URL parser', () => {
441
+ expect(isHrefAllowedByMode('/\\@evil.com', 'internal')).toBe(false);
442
+ });
443
+
444
+ it('allows root path (/)', () => {
445
+ expect(isHrefAllowedByMode('/', 'internal')).toBe(true);
446
+ });
447
+
448
+ it('allows paths with query parameters', () => {
449
+ expect(isHrefAllowedByMode('/search?q=hello', 'internal')).toBe(true);
450
+ });
451
+
452
+ it('allows paths with hash fragments', () => {
453
+ expect(isHrefAllowedByMode('/page#section', 'internal')).toBe(true);
454
+ });
455
+ });
456
+ });
457
+
458
+ describe('all mode', () => {
459
+ it('allows root-relative paths', () => {
460
+ expect(isHrefAllowedByMode('/settings', 'all')).toBe(true);
461
+ });
462
+
463
+ it('allows external domains', () => {
464
+ expect(isHrefAllowedByMode('https://example.com', 'all')).toBe(true);
465
+ });
466
+
467
+ it('allows wise.com URLs', () => {
468
+ expect(isHrefAllowedByMode('https://wise.com/help', 'all')).toBe(true);
469
+ });
470
+
471
+ it('allows hash fragments', () => {
472
+ expect(isHrefAllowedByMode('#section', 'all')).toBe(true);
473
+ });
474
+
475
+ it('allows mailto links', () => {
476
+ expect(isHrefAllowedByMode('mailto:user@example.com', 'all')).toBe(true);
477
+ });
478
+ });
479
+
480
+ describe('none mode', () => {
481
+ it('blocks root-relative paths', () => {
482
+ expect(isHrefAllowedByMode('/accounts', 'none')).toBe(false);
483
+ });
484
+
485
+ it('blocks wise.com URLs', () => {
486
+ expect(isHrefAllowedByMode('https://wise.com/help', 'none')).toBe(false);
487
+ });
488
+
489
+ it('blocks external domains', () => {
490
+ expect(isHrefAllowedByMode('https://example.com', 'none')).toBe(false);
491
+ });
492
+
493
+ it('blocks hash fragments', () => {
494
+ expect(isHrefAllowedByMode('#section', 'none')).toBe(false);
495
+ });
496
+ });
497
+ });
@@ -0,0 +1,133 @@
1
+ import { ALLOWED_PROTOCOLS } from '../constants';
2
+
3
+ const INVISIBLE_CHARS = /[\u200b\u200c\u200d\ufeff\u00ad\u2060\u180e]/g;
4
+ // eslint-disable-next-line regexp/no-control-character
5
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
6
+ const DANGEROUS_HTML_CHARS = /[<>"']/;
7
+ const DANGEROUS_SCHEME = /^(?:javascript|vbscript|data|jscript)\s*:/i;
8
+ // eslint-disable-next-line regexp/no-useless-non-capturing-group
9
+ const ENCODED_COLON_SCHEME = /^(?:javascript|vbscript|data|jscript)(?:%3a)/i;
10
+ const IPV6_BRACKETS = /^\[|\]$/g;
11
+ const NUMERIC_IP = /^[\d.]+$/;
12
+ const HEX_IP = /^0x[0-9a-f]+$/i;
13
+
14
+ export function isAllowedHref(href: string): boolean {
15
+ const cleaned = href.replace(INVISIBLE_CHARS, '');
16
+ const trimmed = cleaned.trim();
17
+
18
+ if (!trimmed) {
19
+ return false;
20
+ }
21
+
22
+ if (CONTROL_CHARS.test(trimmed)) {
23
+ return false;
24
+ }
25
+
26
+ if (DANGEROUS_HTML_CHARS.test(trimmed)) {
27
+ return false;
28
+ }
29
+
30
+ if (DANGEROUS_SCHEME.test(trimmed)) {
31
+ return false;
32
+ }
33
+
34
+ if (ENCODED_COLON_SCHEME.test(trimmed)) {
35
+ return false;
36
+ }
37
+
38
+ if (trimmed.startsWith('//')) {
39
+ return false;
40
+ }
41
+
42
+ if (trimmed.startsWith('\\')) {
43
+ return false;
44
+ }
45
+
46
+ if (trimmed.startsWith('/')) {
47
+ return trimmed[1] !== '\\';
48
+ }
49
+
50
+ if (trimmed.startsWith('#')) {
51
+ return true;
52
+ }
53
+
54
+ if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
55
+ return false;
56
+ }
57
+
58
+ try {
59
+ const url = new URL(trimmed);
60
+
61
+ if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
62
+ return false;
63
+ }
64
+
65
+ if (url.protocol === 'https:') {
66
+ if (!trimmed.startsWith('https://')) {
67
+ return false;
68
+ }
69
+
70
+ if (!url.hostname) {
71
+ return false;
72
+ }
73
+
74
+ if (url.username || url.password) {
75
+ return false;
76
+ }
77
+
78
+ if (isInternalHost(url.hostname)) {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ if (url.protocol === 'mailto:') {
84
+ if (url.search) {
85
+ return false;
86
+ }
87
+
88
+ if (!url.pathname) {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ export type AllowLinksMode = 'internal' | 'all' | 'none';
100
+
101
+ // Synthetic origin used to resolve relative paths via the WHATWG URL parser.
102
+ // If the resolved origin still equals this sentinel, the path stays on-origin.
103
+ const SSR_SENTINEL_ORIGIN = 'https://internal.invalid';
104
+
105
+ export function isHrefAllowedByMode(href: string, mode: AllowLinksMode): boolean {
106
+ switch (mode) {
107
+ case 'none':
108
+ return false;
109
+ case 'all':
110
+ return true;
111
+ case 'internal':
112
+ if (href.startsWith('https://wise.com/')) return true;
113
+ if (href.startsWith('/')) {
114
+ try {
115
+ return new URL(href, SSR_SENTINEL_ORIGIN).origin === SSR_SENTINEL_ORIGIN;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+ return false;
121
+ }
122
+ }
123
+
124
+ function isInternalHost(hostname: string): boolean {
125
+ const stripped = hostname.replace(IPV6_BRACKETS, '');
126
+ const host = stripped.endsWith('.') ? stripped.slice(0, -1) : stripped;
127
+ if (NUMERIC_IP.test(host)) return true;
128
+ if (HEX_IP.test(host)) return true;
129
+ if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true;
130
+ if (host === 'localhost') return true;
131
+ if (host.endsWith('.internal')) return true;
132
+ return false;
133
+ }
@@ -6,5 +6,3 @@ export { isDateValid } from './isDateValid/isDateValid';
6
6
  export { isMonthAndYearFormat } from './isMonthAndYearFormat/isMonthAndYearFormat';
7
7
  export { MDY, YMD } from './getFormatForLocale/getFormatForLocale';
8
8
  export { returnDateView } from './getDateView/getDateView';
9
- export { isDateDisabled } from './isDateDisabled/isDateDisabled';
10
- export { hasDisabledDatesInMonth } from './hasDisabledDatesInMonth/hasDisabledDatesInMonth';
package/src/i18n/en.json CHANGED
@@ -12,10 +12,6 @@
12
12
  "neptune.DateLookup.goTo20YearView": "Go to 20 year view",
13
13
  "neptune.DateLookup.month": "month",
14
14
  "neptune.DateLookup.next": "next",
15
- "neptune.DateLookup.notAvailable": "not available",
16
- "neptune.DateLookup.outOfRangeAfter": "Please select a date on or after {min}.",
17
- "neptune.DateLookup.outOfRangeBefore": "Please select a date on or before {max}.",
18
- "neptune.DateLookup.outOfRangeBetween": "Please select a date between {min} and {max}.",
19
15
  "neptune.DateLookup.previous": "previous",
20
16
  "neptune.DateLookup.selected": "selected",
21
17
  "neptune.DateLookup.twentyYears": "20 years",
package/src/index.ts CHANGED
@@ -25,8 +25,6 @@ export type {
25
25
  ButtonProps as LegacyButtonProps,
26
26
  AnchorElementProps,
27
27
  } from './Button';
28
- export type { CalendarProps } from './Calendar';
29
- export type { DisabledRules, WeekDayType, DisabledDateRange } from './Calendar';
30
28
  export type { CardProps } from './_deprecated/Card';
31
29
  export type {
32
30
  CarouselProps,
@@ -57,6 +55,7 @@ export type {
57
55
  export type { BottomSheetProps } from './common/BottomSheet';
58
56
  export type { DateInputProps } from './DateInput';
59
57
  export type { DateLookupProps } from './DateLookup';
58
+ export type { CalendarProps } from './Calendar';
60
59
  export type { DecisionProps } from './Decision';
61
60
  export type {
62
61
  DefinitionListProps,
@@ -92,6 +91,7 @@ export type { LabelProps, LabelOptionalProps, LabelDescriptionProps } from './La
92
91
  export type { LegacyListItemProps } from './_deprecated/LegacyListItem';
93
92
  export type { LoaderProps } from './Loader';
94
93
  export type { MarkdownProps } from './Markdown';
94
+ export type { MarkupActionHandler, MarkupActions, MarkupNode, MarkupProps } from './Markup';
95
95
  export type { ModalProps } from './Modal';
96
96
  export type { MoneyProps } from './Money';
97
97
  export type {
@@ -202,7 +202,6 @@ export { default as Badge, BadgeAssets } from './_deprecated/Badge';
202
202
  export { default as Body } from './Body';
203
203
  export { default as Button } from './Button';
204
204
  export { default as IconButton } from './IconButton';
205
- export { default as Calendar } from './Calendar';
206
205
  export { default as Carousel } from './Carousel';
207
206
  export { default as Card } from './_deprecated/Card';
208
207
  export { default as Checkbox } from './Checkbox';
@@ -218,6 +217,7 @@ export { default as BaseCard } from './common/BaseCard';
218
217
  export { default as CriticalCommsBanner } from './_deprecated/CriticalBanner';
219
218
  export { default as DateInput } from './DateInput';
220
219
  export { default as DateLookup } from './DateLookup';
220
+ export { default as Calendar } from './Calendar';
221
221
  export { default as Decision } from './Decision';
222
222
  export { default as DefinitionList } from './DefinitionList';
223
223
  export { default as Dimmer } from './Dimmer';
@@ -250,6 +250,7 @@ export { default as LegacyListItem } from './_deprecated/LegacyListItem';
250
250
  export { default as Loader } from './Loader';
251
251
  export { default as Logo } from './Logo';
252
252
  export { default as Markdown } from './Markdown';
253
+ export { default as Markup } from './Markup';
253
254
  export { default as Modal } from './Modal';
254
255
  export { default as Money } from './Money';
255
256
  export { default as MoneyInput } from './MoneyInput';