@multiplatform.one/wallet 7.7.2

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 (155) hide show
  1. package/README.md +8 -0
  2. package/dist/cjs/SiweKeycloakProvider.cjs +273 -0
  3. package/dist/cjs/SiweKeycloakProvider.native.js +309 -0
  4. package/dist/cjs/SiweKeycloakProvider.native.js.map +1 -0
  5. package/dist/cjs/base64.cjs +51 -0
  6. package/dist/cjs/base64.native.js +55 -0
  7. package/dist/cjs/base64.native.js.map +1 -0
  8. package/dist/cjs/base64.spec.cjs +57 -0
  9. package/dist/cjs/base64.spec.native.js +79 -0
  10. package/dist/cjs/base64.spec.native.js.map +1 -0
  11. package/dist/cjs/context.cjs +29 -0
  12. package/dist/cjs/context.native.js +32 -0
  13. package/dist/cjs/context.native.js.map +1 -0
  14. package/dist/cjs/hooks/context.cjs +29 -0
  15. package/dist/cjs/hooks/context.native.js +32 -0
  16. package/dist/cjs/hooks/context.native.js.map +1 -0
  17. package/dist/cjs/hooks/index.cjs +20 -0
  18. package/dist/cjs/hooks/index.native.js +23 -0
  19. package/dist/cjs/hooks/index.native.js.map +1 -0
  20. package/dist/cjs/hooks/useSiweKeycloak.cjs +61 -0
  21. package/dist/cjs/hooks/useSiweKeycloak.native.js +64 -0
  22. package/dist/cjs/hooks/useSiweKeycloak.native.js.map +1 -0
  23. package/dist/cjs/index.cjs +133 -0
  24. package/dist/cjs/index.native.js +189 -0
  25. package/dist/cjs/index.native.js.map +1 -0
  26. package/dist/cjs/render.cjs +78 -0
  27. package/dist/cjs/render.native.js +82 -0
  28. package/dist/cjs/render.native.js.map +1 -0
  29. package/dist/cjs/siwe.cjs +76 -0
  30. package/dist/cjs/siwe.native.js +94 -0
  31. package/dist/cjs/siwe.native.js.map +1 -0
  32. package/dist/cjs/types.cjs +18 -0
  33. package/dist/cjs/types.native.js +21 -0
  34. package/dist/cjs/types.native.js.map +1 -0
  35. package/dist/cjs/util.cjs +53 -0
  36. package/dist/cjs/util.native.js +56 -0
  37. package/dist/cjs/util.native.js.map +1 -0
  38. package/dist/cjs/util.spec.cjs +53 -0
  39. package/dist/cjs/util.spec.native.js +62 -0
  40. package/dist/cjs/util.spec.native.js.map +1 -0
  41. package/dist/esm/SiweKeycloakProvider.mjs +237 -0
  42. package/dist/esm/SiweKeycloakProvider.mjs.map +1 -0
  43. package/dist/esm/SiweKeycloakProvider.native.js +270 -0
  44. package/dist/esm/SiweKeycloakProvider.native.js.map +1 -0
  45. package/dist/esm/base64.mjs +26 -0
  46. package/dist/esm/base64.mjs.map +1 -0
  47. package/dist/esm/base64.native.js +27 -0
  48. package/dist/esm/base64.native.js.map +1 -0
  49. package/dist/esm/base64.spec.mjs +58 -0
  50. package/dist/esm/base64.spec.mjs.map +1 -0
  51. package/dist/esm/base64.spec.native.js +77 -0
  52. package/dist/esm/base64.spec.native.js.map +1 -0
  53. package/dist/esm/context.mjs +4 -0
  54. package/dist/esm/context.mjs.map +1 -0
  55. package/dist/esm/context.native.js +4 -0
  56. package/dist/esm/context.native.js.map +1 -0
  57. package/dist/esm/hooks/context.mjs +4 -0
  58. package/dist/esm/hooks/context.mjs.map +1 -0
  59. package/dist/esm/hooks/context.native.js +4 -0
  60. package/dist/esm/hooks/context.native.js.map +1 -0
  61. package/dist/esm/hooks/index.mjs +2 -0
  62. package/dist/esm/hooks/index.mjs.map +1 -0
  63. package/dist/esm/hooks/index.native.js +2 -0
  64. package/dist/esm/hooks/index.native.js.map +1 -0
  65. package/dist/esm/hooks/useSiweKeycloak.mjs +36 -0
  66. package/dist/esm/hooks/useSiweKeycloak.mjs.map +1 -0
  67. package/dist/esm/hooks/useSiweKeycloak.native.js +36 -0
  68. package/dist/esm/hooks/useSiweKeycloak.native.js.map +1 -0
  69. package/dist/esm/index.mjs +107 -0
  70. package/dist/esm/index.mjs.map +1 -0
  71. package/dist/esm/index.native.js +160 -0
  72. package/dist/esm/index.native.js.map +1 -0
  73. package/dist/esm/render.mjs +42 -0
  74. package/dist/esm/render.mjs.map +1 -0
  75. package/dist/esm/render.native.js +43 -0
  76. package/dist/esm/render.native.js.map +1 -0
  77. package/dist/esm/siwe.mjs +51 -0
  78. package/dist/esm/siwe.mjs.map +1 -0
  79. package/dist/esm/siwe.native.js +66 -0
  80. package/dist/esm/siwe.native.js.map +1 -0
  81. package/dist/esm/types.mjs +2 -0
  82. package/dist/esm/types.mjs.map +1 -0
  83. package/dist/esm/types.native.js +2 -0
  84. package/dist/esm/types.native.js.map +1 -0
  85. package/dist/esm/util.mjs +27 -0
  86. package/dist/esm/util.mjs.map +1 -0
  87. package/dist/esm/util.native.js +27 -0
  88. package/dist/esm/util.native.js.map +1 -0
  89. package/dist/esm/util.spec.mjs +54 -0
  90. package/dist/esm/util.spec.mjs.map +1 -0
  91. package/dist/esm/util.spec.native.js +60 -0
  92. package/dist/esm/util.spec.native.js.map +1 -0
  93. package/package.json +81 -0
  94. package/src/SiweKeycloakProvider.tsx +297 -0
  95. package/src/base64.spec.ts +65 -0
  96. package/src/base64.ts +26 -0
  97. package/src/context.ts +10 -0
  98. package/src/hooks/context.ts +10 -0
  99. package/src/hooks/index.ts +1 -0
  100. package/src/hooks/useSiweKeycloak.ts +23 -0
  101. package/src/index.ts +127 -0
  102. package/src/main/java/com/bitspur/keycloak/siwe/Errors.java +21 -0
  103. package/src/main/java/com/bitspur/keycloak/siwe/LiteAuthenticationService.java +167 -0
  104. package/src/main/java/com/bitspur/keycloak/siwe/SiweCredentials.java +36 -0
  105. package/src/main/java/com/bitspur/keycloak/siwe/SiweSession.java +36 -0
  106. package/src/main/java/com/bitspur/keycloak/siwe/SiweUtil.java +599 -0
  107. package/src/main/java/com/bitspur/keycloak/siwe/Token.java +85 -0
  108. package/src/main/java/com/bitspur/keycloak/siwe/authenticators/browser/SiweBrowserAuthenticator.java +109 -0
  109. package/src/main/java/com/bitspur/keycloak/siwe/authenticators/browser/SiweBrowserAuthenticatorFactory.java +93 -0
  110. package/src/main/java/com/bitspur/keycloak/siwe/authenticators/directgrant/SiweDirectGrantAuthenticator.java +153 -0
  111. package/src/main/java/com/bitspur/keycloak/siwe/authenticators/directgrant/SiweDirectGrantAuthenticatorFactory.java +93 -0
  112. package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonce.java +95 -0
  113. package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonceJpaEntityProvider.java +82 -0
  114. package/src/main/java/com/bitspur/keycloak/siwe/jpa/nonce/SiweNonceJpaEntityProviderFactory.java +46 -0
  115. package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletAddress.java +103 -0
  116. package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletJpaEntityProvider.java +43 -0
  117. package/src/main/java/com/bitspur/keycloak/siwe/jpa/wallet/SiweWalletJpaEntityProviderFactory.java +46 -0
  118. package/src/main/java/com/bitspur/keycloak/siwe/protocol/SiweWalletProtocolMapper.java +85 -0
  119. package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResource.java +210 -0
  120. package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResourceProvider.java +34 -0
  121. package/src/main/java/com/bitspur/keycloak/siwe/resource/SiweResourceProviderFactory.java +46 -0
  122. package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuCredentials.java +52 -0
  123. package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuMessage.java +362 -0
  124. package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuParseException.java +23 -0
  125. package/src/main/java/com/bitspur/keycloak/siwe/siu/SiuUtil.java +547 -0
  126. package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/browser/SiuBrowserAuthenticator.java +112 -0
  127. package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/browser/SiuBrowserAuthenticatorFactory.java +96 -0
  128. package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/directgrant/SiuDirectGrantAuthenticator.java +169 -0
  129. package/src/main/java/com/bitspur/keycloak/siwe/siu/authenticators/directgrant/SiuDirectGrantAuthenticatorFactory.java +96 -0
  130. package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResource.java +244 -0
  131. package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResourceProvider.java +34 -0
  132. package/src/main/java/com/bitspur/keycloak/siwe/siu/resource/SiuResourceProviderFactory.java +48 -0
  133. package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsCredentials.java +52 -0
  134. package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsMessage.java +362 -0
  135. package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsParseException.java +23 -0
  136. package/src/main/java/com/bitspur/keycloak/siwe/siws/SiwsUtil.java +564 -0
  137. package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/browser/SiwsBrowserAuthenticator.java +112 -0
  138. package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/browser/SiwsBrowserAuthenticatorFactory.java +96 -0
  139. package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/directgrant/SiwsDirectGrantAuthenticator.java +169 -0
  140. package/src/main/java/com/bitspur/keycloak/siwe/siws/authenticators/directgrant/SiwsDirectGrantAuthenticatorFactory.java +96 -0
  141. package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResource.java +244 -0
  142. package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProvider.java +34 -0
  143. package/src/main/java/com/bitspur/keycloak/siwe/siws/resource/SiwsResourceProviderFactory.java +48 -0
  144. package/src/main/resources/META-INF/keycloak-themes.json +8 -0
  145. package/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +6 -0
  146. package/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory +2 -0
  147. package/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +1 -0
  148. package/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory +3 -0
  149. package/src/main/resources/META-INF/siwe-nonce-changelog.xml +29 -0
  150. package/src/main/resources/META-INF/siwe-wallet-changelog.xml +46 -0
  151. package/src/render.tsx +47 -0
  152. package/src/siwe.tsx +57 -0
  153. package/src/types.ts +6 -0
  154. package/src/util.spec.ts +72 -0
  155. package/src/util.ts +28 -0
@@ -0,0 +1,362 @@
1
+ /**
2
+ * File: /src/main/java/com/bitspur/keycloak/siwe/siu/SiuMessage.java
3
+ * Project: siwe-keycloak
4
+ * File Created: 24-01-2026
5
+ * Author: Clay Risser
6
+ * -----
7
+ * BitSpur (c) Copyright 2022 - 2026
8
+ *
9
+ * Sign-In With Sui (SIU) message parser
10
+ * Based on EIP-4361 / CAIP-122 specification adapted for Sui
11
+ */
12
+
13
+ package com.bitspur.keycloak.siwe.siu;
14
+
15
+ import java.time.OffsetDateTime;
16
+ import java.time.format.DateTimeFormatter;
17
+ import java.time.format.DateTimeParseException;
18
+ import java.util.ArrayList;
19
+ import java.util.List;
20
+ import java.util.regex.Matcher;
21
+ import java.util.regex.Pattern;
22
+
23
+ /**
24
+ * Represents a parsed SIU (Sign-In With Sui) message.
25
+ *
26
+ * SIU message format follows EIP-4361 / CAIP-122:
27
+ * <pre>
28
+ * ${domain} wants you to sign in with your Sui account:
29
+ * ${address}
30
+ *
31
+ * ${statement}
32
+ *
33
+ * URI: ${uri}
34
+ * Version: ${version}
35
+ * Chain ID: ${chainId}
36
+ * Nonce: ${nonce}
37
+ * Issued At: ${issuedAt}
38
+ * Expiration Time: ${expirationTime}
39
+ * Not Before: ${notBefore}
40
+ * Request ID: ${requestId}
41
+ * Resources:
42
+ * - ${resource1}
43
+ * - ${resource2}
44
+ * </pre>
45
+ */
46
+ public class SiuMessage {
47
+ // Sui address: Hex encoded, 0x followed by 64 hex characters (32 bytes)
48
+ private static final Pattern SUI_ADDRESS_PATTERN = Pattern.compile("^0x[a-fA-F0-9]{64}$");
49
+
50
+ private String domain;
51
+ private String address;
52
+ private String statement;
53
+ private String uri;
54
+ private String version;
55
+ private String chainId;
56
+ private String nonce;
57
+ private OffsetDateTime issuedAt;
58
+ private OffsetDateTime expirationTime;
59
+ private OffsetDateTime notBefore;
60
+ private String requestId;
61
+ private List<String> resources;
62
+
63
+ private SiuMessage() {
64
+ this.resources = new ArrayList<>();
65
+ }
66
+
67
+ public String getDomain() {
68
+ return domain;
69
+ }
70
+
71
+ public String getAddress() {
72
+ return address;
73
+ }
74
+
75
+ public String getStatement() {
76
+ return statement;
77
+ }
78
+
79
+ public String getUri() {
80
+ return uri;
81
+ }
82
+
83
+ public String getVersion() {
84
+ return version;
85
+ }
86
+
87
+ public String getChainId() {
88
+ return chainId;
89
+ }
90
+
91
+ public String getNonce() {
92
+ return nonce;
93
+ }
94
+
95
+ public OffsetDateTime getIssuedAt() {
96
+ return issuedAt;
97
+ }
98
+
99
+ public OffsetDateTime getExpirationTime() {
100
+ return expirationTime;
101
+ }
102
+
103
+ public OffsetDateTime getNotBefore() {
104
+ return notBefore;
105
+ }
106
+
107
+ public String getRequestId() {
108
+ return requestId;
109
+ }
110
+
111
+ public List<String> getResources() {
112
+ return resources;
113
+ }
114
+
115
+ /**
116
+ * Validates that the Sui address is in the correct format.
117
+ * Sui addresses are hex-encoded Ed25519 public keys (0x + 64 hex chars).
118
+ */
119
+ public static boolean isValidSuiAddress(String address) {
120
+ if (address == null || address.isEmpty()) {
121
+ return false;
122
+ }
123
+ return SUI_ADDRESS_PATTERN.matcher(address).matches();
124
+ }
125
+
126
+ /**
127
+ * Parser for SIU messages.
128
+ */
129
+ public static class Parser {
130
+ // ABNF grammar patterns based on EIP-4361 / CAIP-122
131
+ private static final Pattern DOMAIN_PATTERN = Pattern.compile(
132
+ "^(.+) wants you to sign in with your Sui account:$",
133
+ Pattern.MULTILINE
134
+ );
135
+ private static final Pattern ADDRESS_PATTERN = Pattern.compile(
136
+ "^(0x[a-fA-F0-9]{64})$",
137
+ Pattern.MULTILINE
138
+ );
139
+ private static final Pattern URI_PATTERN = Pattern.compile("^URI: (.+)$", Pattern.MULTILINE);
140
+ private static final Pattern VERSION_PATTERN = Pattern.compile("^Version: (.+)$", Pattern.MULTILINE);
141
+ private static final Pattern CHAIN_ID_PATTERN = Pattern.compile("^Chain ID: (.+)$", Pattern.MULTILINE);
142
+ private static final Pattern NONCE_PATTERN = Pattern.compile("^Nonce: ([A-Za-z0-9]{8,})$", Pattern.MULTILINE);
143
+ private static final Pattern ISSUED_AT_PATTERN = Pattern.compile("^Issued At: (.+)$", Pattern.MULTILINE);
144
+ private static final Pattern EXPIRATION_TIME_PATTERN = Pattern.compile("^Expiration Time: (.+)$", Pattern.MULTILINE);
145
+ private static final Pattern NOT_BEFORE_PATTERN = Pattern.compile("^Not Before: (.+)$", Pattern.MULTILINE);
146
+ private static final Pattern REQUEST_ID_PATTERN = Pattern.compile("^Request ID: (.+)$", Pattern.MULTILINE);
147
+ private static final Pattern RESOURCES_PATTERN = Pattern.compile("^Resources:$", Pattern.MULTILINE);
148
+ private static final Pattern RESOURCE_ITEM_PATTERN = Pattern.compile("^- (.+)$", Pattern.MULTILINE);
149
+
150
+ /**
151
+ * Parses a SIU message string into a SiuMessage object.
152
+ *
153
+ * @param message The raw SIU message string
154
+ * @return Parsed SiuMessage
155
+ * @throws SiuParseException If the message cannot be parsed
156
+ */
157
+ public SiuMessage parse(String message) throws SiuParseException {
158
+ if (message == null || message.trim().isEmpty()) {
159
+ throw new SiuParseException("Message cannot be null or empty");
160
+ }
161
+
162
+ SiuMessage siu = new SiuMessage();
163
+ String[] lines = message.split("\n");
164
+
165
+ if (lines.length < 2) {
166
+ throw new SiuParseException("Invalid SIU message format: too few lines");
167
+ }
168
+
169
+ // Parse domain from first line
170
+ Matcher domainMatcher = DOMAIN_PATTERN.matcher(lines[0]);
171
+ if (!domainMatcher.matches()) {
172
+ throw new SiuParseException("Invalid SIU message: missing or invalid domain line");
173
+ }
174
+ siu.domain = domainMatcher.group(1).trim();
175
+
176
+ // Parse address from second line
177
+ if (lines.length < 2 || !ADDRESS_PATTERN.matcher(lines[1].trim()).matches()) {
178
+ throw new SiuParseException("Invalid SIU message: missing or invalid Sui address");
179
+ }
180
+ siu.address = lines[1].trim();
181
+
182
+ // Parse statement (optional, between address and first field)
183
+ StringBuilder statementBuilder = new StringBuilder();
184
+ int fieldStartIndex = -1;
185
+ for (int i = 2; i < lines.length; i++) {
186
+ String line = lines[i];
187
+ if (line.startsWith("URI:") || line.startsWith("Version:") ||
188
+ line.startsWith("Chain ID:") || line.startsWith("Nonce:") ||
189
+ line.startsWith("Issued At:") || line.startsWith("Expiration Time:") ||
190
+ line.startsWith("Not Before:") || line.startsWith("Request ID:") ||
191
+ line.startsWith("Resources:")) {
192
+ fieldStartIndex = i;
193
+ break;
194
+ }
195
+ // Skip empty lines at the start/end of statement
196
+ if (statementBuilder.length() > 0 || !line.trim().isEmpty()) {
197
+ if (statementBuilder.length() > 0) {
198
+ statementBuilder.append("\n");
199
+ }
200
+ statementBuilder.append(line);
201
+ }
202
+ }
203
+ String statement = statementBuilder.toString().trim();
204
+ if (!statement.isEmpty()) {
205
+ siu.statement = statement;
206
+ }
207
+
208
+ // Parse remaining fields
209
+ String remainingMessage = fieldStartIndex > 0
210
+ ? String.join("\n", java.util.Arrays.copyOfRange(lines, fieldStartIndex, lines.length))
211
+ : "";
212
+
213
+ // Required fields
214
+ siu.uri = extractField(remainingMessage, URI_PATTERN, "URI");
215
+ siu.version = extractField(remainingMessage, VERSION_PATTERN, "Version");
216
+ siu.nonce = extractField(remainingMessage, NONCE_PATTERN, "Nonce");
217
+ siu.issuedAt = parseDateTime(extractField(remainingMessage, ISSUED_AT_PATTERN, "Issued At"));
218
+
219
+ // Optional fields
220
+ String chainIdStr = extractFieldOptional(remainingMessage, CHAIN_ID_PATTERN);
221
+ if (chainIdStr != null) {
222
+ siu.chainId = chainIdStr;
223
+ }
224
+
225
+ String expirationTimeStr = extractFieldOptional(remainingMessage, EXPIRATION_TIME_PATTERN);
226
+ if (expirationTimeStr != null) {
227
+ siu.expirationTime = parseDateTime(expirationTimeStr);
228
+ }
229
+
230
+ String notBeforeStr = extractFieldOptional(remainingMessage, NOT_BEFORE_PATTERN);
231
+ if (notBeforeStr != null) {
232
+ siu.notBefore = parseDateTime(notBeforeStr);
233
+ }
234
+
235
+ String requestIdStr = extractFieldOptional(remainingMessage, REQUEST_ID_PATTERN);
236
+ if (requestIdStr != null) {
237
+ siu.requestId = requestIdStr;
238
+ }
239
+
240
+ // Parse resources (optional)
241
+ Matcher resourcesMatcher = RESOURCES_PATTERN.matcher(remainingMessage);
242
+ if (resourcesMatcher.find()) {
243
+ int resourcesStart = resourcesMatcher.end();
244
+ String resourcesSection = remainingMessage.substring(resourcesStart);
245
+ Matcher resourceItemMatcher = RESOURCE_ITEM_PATTERN.matcher(resourcesSection);
246
+ while (resourceItemMatcher.find()) {
247
+ siu.resources.add(resourceItemMatcher.group(1).trim());
248
+ }
249
+ }
250
+
251
+ // Validate the message
252
+ validate(siu);
253
+
254
+ return siu;
255
+ }
256
+
257
+ private String extractField(String message, Pattern pattern, String fieldName) throws SiuParseException {
258
+ Matcher matcher = pattern.matcher(message);
259
+ if (!matcher.find()) {
260
+ throw new SiuParseException("Missing required field: " + fieldName);
261
+ }
262
+ return matcher.group(1).trim();
263
+ }
264
+
265
+ private String extractFieldOptional(String message, Pattern pattern) {
266
+ Matcher matcher = pattern.matcher(message);
267
+ if (matcher.find()) {
268
+ return matcher.group(1).trim();
269
+ }
270
+ return null;
271
+ }
272
+
273
+ private OffsetDateTime parseDateTime(String dateTimeStr) throws SiuParseException {
274
+ if (dateTimeStr == null || dateTimeStr.isEmpty()) {
275
+ return null;
276
+ }
277
+ try {
278
+ return OffsetDateTime.parse(dateTimeStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
279
+ } catch (DateTimeParseException e) {
280
+ // Try parsing without timezone offset
281
+ try {
282
+ return OffsetDateTime.parse(dateTimeStr + "Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME);
283
+ } catch (DateTimeParseException e2) {
284
+ throw new SiuParseException("Invalid datetime format: " + dateTimeStr);
285
+ }
286
+ }
287
+ }
288
+
289
+ private void validate(SiuMessage siu) throws SiuParseException {
290
+ // Validate Sui address format
291
+ if (!SiuMessage.isValidSuiAddress(siu.address)) {
292
+ throw new SiuParseException("Invalid Sui address format: " + siu.address);
293
+ }
294
+
295
+ // Validate version is "1"
296
+ if (!"1".equals(siu.version)) {
297
+ throw new SiuParseException("Unsupported SIU version: " + siu.version + " (expected: 1)");
298
+ }
299
+
300
+ // Validate nonce length (at least 8 alphanumeric characters)
301
+ if (siu.nonce == null || siu.nonce.length() < 8) {
302
+ throw new SiuParseException("Nonce must be at least 8 alphanumeric characters");
303
+ }
304
+
305
+ // Validate expiration time if present
306
+ if (siu.expirationTime != null && siu.expirationTime.isBefore(OffsetDateTime.now())) {
307
+ throw new SiuParseException("Message has expired");
308
+ }
309
+
310
+ // Validate not before if present
311
+ if (siu.notBefore != null && siu.notBefore.isAfter(OffsetDateTime.now())) {
312
+ throw new SiuParseException("Message is not yet valid (notBefore is in the future)");
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Reconstructs the SIU message from its components.
319
+ * Used for signature verification to ensure message integrity.
320
+ */
321
+ public String toMessage() {
322
+ StringBuilder sb = new StringBuilder();
323
+ sb.append(domain).append(" wants you to sign in with your Sui account:\n");
324
+ sb.append(address).append("\n");
325
+
326
+ if (statement != null && !statement.isEmpty()) {
327
+ sb.append("\n").append(statement).append("\n");
328
+ }
329
+
330
+ sb.append("\n");
331
+ sb.append("URI: ").append(uri).append("\n");
332
+ sb.append("Version: ").append(version).append("\n");
333
+
334
+ if (chainId != null) {
335
+ sb.append("Chain ID: ").append(chainId).append("\n");
336
+ }
337
+
338
+ sb.append("Nonce: ").append(nonce).append("\n");
339
+ sb.append("Issued At: ").append(issuedAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).append("\n");
340
+
341
+ if (expirationTime != null) {
342
+ sb.append("Expiration Time: ").append(expirationTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).append("\n");
343
+ }
344
+
345
+ if (notBefore != null) {
346
+ sb.append("Not Before: ").append(notBefore.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).append("\n");
347
+ }
348
+
349
+ if (requestId != null) {
350
+ sb.append("Request ID: ").append(requestId).append("\n");
351
+ }
352
+
353
+ if (resources != null && !resources.isEmpty()) {
354
+ sb.append("Resources:\n");
355
+ for (String resource : resources) {
356
+ sb.append("- ").append(resource).append("\n");
357
+ }
358
+ }
359
+
360
+ return sb.toString();
361
+ }
362
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * File: /src/main/java/com/bitspur/keycloak/siwe/siu/SiuParseException.java
3
+ * Project: siwe-keycloak
4
+ * File Created: 24-01-2026
5
+ * Author: Clay Risser
6
+ * -----
7
+ * BitSpur (c) Copyright 2022 - 2026
8
+ */
9
+
10
+ package com.bitspur.keycloak.siwe.siu;
11
+
12
+ /**
13
+ * Exception thrown when parsing a SIU message fails.
14
+ */
15
+ public class SiuParseException extends Exception {
16
+ public SiuParseException(String message) {
17
+ super(message);
18
+ }
19
+
20
+ public SiuParseException(String message, Throwable cause) {
21
+ super(message, cause);
22
+ }
23
+ }