@qntx/siwx 0.0.0-stage → 0.7.1
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.
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +15 -2
- package/dist/eip4361-XA-TUOKF.mjs +125 -0
- package/dist/evm.d.mts +41 -0
- package/dist/evm.mjs +289 -0
- package/dist/index.d.mts +113 -0
- package/dist/index.mjs +32 -0
- package/dist/message-ZewmOzps.mjs +510 -0
- package/dist/namespace-Cl7PJYC-.mjs +68 -0
- package/dist/namespace-DlgXqLsI.d.mts +105 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/svm.d.mts +42 -0
- package/dist/svm.mjs +329 -0
- package/package.json +84 -4
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
//#region src/core/error.ts
|
|
2
|
+
const SYNTAX_MESSAGES = {
|
|
3
|
+
InvalidUtf8: "invalid UTF-8",
|
|
4
|
+
CarriageReturn: "CR not allowed",
|
|
5
|
+
MissingPreamble: "missing preamble marker",
|
|
6
|
+
MissingAccountSuffix: "missing account suffix",
|
|
7
|
+
UnexpectedEof: "unexpected end of input",
|
|
8
|
+
ExpectedBlankLine: "expected blank line",
|
|
9
|
+
UnexpectedTrailing: "unexpected trailing content",
|
|
10
|
+
ResourceSyntax: "resource line must start with '- '",
|
|
11
|
+
InvalidEnvelope: "invalid off-chain message envelope"
|
|
12
|
+
};
|
|
13
|
+
function describe(info) {
|
|
14
|
+
if (info.code === "MessageTooLarge") return `message exceeds maximum size of ${info.max} bytes, got ${info.length}`;
|
|
15
|
+
if (info.code === "Syntax") return info.syntax === "MissingField" ? `invalid message syntax: missing ${info.field}` : `invalid message syntax: ${SYNTAX_MESSAGES[info.syntax]}`;
|
|
16
|
+
if (info.code === "InvalidField") return `invalid ${info.field}: ${info.reason}`;
|
|
17
|
+
if (info.code === "Mismatch") return `${info.field} mismatch: expected ${info.expected}, got ${JSON.stringify(info.actual)}`;
|
|
18
|
+
if (info.code === "Expired") return "message expired";
|
|
19
|
+
if (info.code === "NotYetValid") return "message not yet valid";
|
|
20
|
+
if (info.code === "IssuedAtTooOld") return "issued-at exceeds max age";
|
|
21
|
+
if (info.code === "InvalidSignature") return `invalid signature: ${info.reason}`;
|
|
22
|
+
if (info.code === "VerificationFailed") return "verification failed";
|
|
23
|
+
if (info.code === "Unsupported") return `unsupported: ${info.reason}`;
|
|
24
|
+
return `backend error: ${info.reason}`;
|
|
25
|
+
}
|
|
26
|
+
/** The single error type thrown by every library failure. */
|
|
27
|
+
var SiwxError = class extends Error {
|
|
28
|
+
/** Structured payload; `code` matches the Rust `Error::code()`. */
|
|
29
|
+
info;
|
|
30
|
+
constructor(info) {
|
|
31
|
+
super(describe(info));
|
|
32
|
+
this.name = "SiwxError";
|
|
33
|
+
this.info = info;
|
|
34
|
+
}
|
|
35
|
+
/** Stable variant name for cross-language contract tests. */
|
|
36
|
+
get code() {
|
|
37
|
+
return this.info.code;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** `SiwxError` constructor shorthand for layout failures. */
|
|
41
|
+
function syntaxError(syntax, field) {
|
|
42
|
+
if (syntax === "MissingField") {
|
|
43
|
+
if (field === void 0) throw new RangeError("MissingField requires a field");
|
|
44
|
+
return new SiwxError({
|
|
45
|
+
code: "Syntax",
|
|
46
|
+
syntax,
|
|
47
|
+
field
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return new SiwxError({
|
|
51
|
+
code: "Syntax",
|
|
52
|
+
syntax
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** `SiwxError` constructor shorthand for a malformed field value. */
|
|
56
|
+
function fieldError(field, reason) {
|
|
57
|
+
return new SiwxError({
|
|
58
|
+
code: "InvalidField",
|
|
59
|
+
field,
|
|
60
|
+
reason
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** `SiwxError` constructor shorthand for a binding mismatch. */
|
|
64
|
+
function mismatchError(field, expected, actual) {
|
|
65
|
+
return new SiwxError({
|
|
66
|
+
code: "Mismatch",
|
|
67
|
+
field,
|
|
68
|
+
expected,
|
|
69
|
+
actual
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/core/account.ts
|
|
74
|
+
//! CAIP-10 `account_id`: `namespace ":" reference ":" address`.
|
|
75
|
+
/** Every character matches — avoids spread-on-string for Hermes-safe lint. */
|
|
76
|
+
function everyChar(s, test) {
|
|
77
|
+
const pred = typeof test === "function" ? test : (c) => test.test(c);
|
|
78
|
+
for (const c of s) if (!pred(c)) return false;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate and build an `account_id`.
|
|
83
|
+
*
|
|
84
|
+
* `namespace` is CAIP-2 `[-a-z0-9]{3,8}`; `reference` is `[-_a-zA-Z0-9]{1,32}`; `address` is
|
|
85
|
+
* `[-.%a-zA-Z0-9]{1,128}`.
|
|
86
|
+
*
|
|
87
|
+
* @throws {SiwxError} `InvalidField` `chainId` for bad namespace/reference, `address` for a bad
|
|
88
|
+
* address.
|
|
89
|
+
*/
|
|
90
|
+
function createAccountId(namespace, reference, address) {
|
|
91
|
+
if (namespace.length < 3 || namespace.length > 8 || !everyChar(namespace, /[a-z0-9-]/)) throw fieldError("chainId", "bad CAIP-2 namespace");
|
|
92
|
+
if (reference.length === 0 || reference.length > 32 || !everyChar(reference, /[A-Za-z0-9_-]/)) throw fieldError("chainId", "bad CAIP-2 reference");
|
|
93
|
+
if (address.length === 0 || address.length > 128 || !everyChar(address, (c) => /[A-Za-z0-9]/.test(c) || c === "-" || c === "." || c === "%")) throw fieldError("address", "bad CAIP-10 address");
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
namespace,
|
|
96
|
+
reference,
|
|
97
|
+
address,
|
|
98
|
+
id: `${namespace}:${reference}:${address}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse `namespace:reference:address`.
|
|
103
|
+
*
|
|
104
|
+
* @throws {SiwxError} `InvalidField` `chainId` when the shape is wrong.
|
|
105
|
+
*/
|
|
106
|
+
function parseAccountId(id) {
|
|
107
|
+
const first = id.indexOf(":");
|
|
108
|
+
const second = first === -1 ? -1 : id.indexOf(":", first + 1);
|
|
109
|
+
if (first === -1 || second < 0) throw fieldError("chainId", "expected namespace:reference:address");
|
|
110
|
+
return createAccountId(id.slice(0, first), id.slice(first + 1, second), id.slice(second + 1));
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/core/layout.ts
|
|
114
|
+
//! Shared pieces of the CAIP-122 text layouts (eip4361 and SIWS): the
|
|
115
|
+
//! preamble infix/suffix and the optional trailing tags in fixed ABNF order.
|
|
116
|
+
const MID = " wants you to sign in with your ";
|
|
117
|
+
const SUFFIX = " account:";
|
|
118
|
+
/**
|
|
119
|
+
* Parse the optional trailing tags in fixed ABNF order: `Expiration Time`, `Not Before`, `Request
|
|
120
|
+
* ID`, `Resources` (with `- ` items). Each may appear at most once and only in this order.
|
|
121
|
+
*/
|
|
122
|
+
function optionalTags(lines, start) {
|
|
123
|
+
let i = start;
|
|
124
|
+
let resources;
|
|
125
|
+
const take = (tag) => {
|
|
126
|
+
const line = lines[i];
|
|
127
|
+
if (line !== void 0 && line.startsWith(tag)) {
|
|
128
|
+
i++;
|
|
129
|
+
return line.slice(tag.length);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const expirationTime = take("Expiration Time: ");
|
|
133
|
+
const notBefore = take("Not Before: ");
|
|
134
|
+
const requestId = take("Request ID: ");
|
|
135
|
+
if (lines[i] === "Resources:") {
|
|
136
|
+
i++;
|
|
137
|
+
const list = [];
|
|
138
|
+
while (i < lines.length) {
|
|
139
|
+
const r = lines[i] ?? "";
|
|
140
|
+
if (r.startsWith("- ")) {
|
|
141
|
+
list.push(r.slice(2));
|
|
142
|
+
i++;
|
|
143
|
+
} else if (r.startsWith("-")) throw syntaxError("ResourceSyntax");
|
|
144
|
+
else throw syntaxError("UnexpectedTrailing");
|
|
145
|
+
}
|
|
146
|
+
resources = list;
|
|
147
|
+
}
|
|
148
|
+
if (i !== lines.length) throw syntaxError("UnexpectedTrailing");
|
|
149
|
+
return {
|
|
150
|
+
expirationTime,
|
|
151
|
+
notBefore,
|
|
152
|
+
requestId,
|
|
153
|
+
resources
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/core/limits.ts
|
|
158
|
+
/** Minimum EIP-4361 nonce length. */
|
|
159
|
+
const MIN_NONCE_LEN = 8;
|
|
160
|
+
/** Maximum accepted signing-text size in bytes. */
|
|
161
|
+
const MAX_MESSAGE_BYTES = 16384;
|
|
162
|
+
/** Maximum number of entries in the `Resources` list. */
|
|
163
|
+
const MAX_RESOURCES = 32;
|
|
164
|
+
/** Maximum accepted `statement` size in bytes. */
|
|
165
|
+
const MAX_STATEMENT_BYTES = 4096;
|
|
166
|
+
/** Maximum accepted `request_id` size in bytes. */
|
|
167
|
+
const MAX_REQUEST_ID_BYTES = 128;
|
|
168
|
+
/** Maximum accepted URI size in bytes (`uri` and each resource). */
|
|
169
|
+
const MAX_URI_BYTES = 2048;
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/core/rfc3986.ts
|
|
172
|
+
//! RFC 3986 URI validation, matching Rust `iri-string` exactly:
|
|
173
|
+
//! `URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]`,
|
|
174
|
+
//! `authority = [ userinfo "@" ] host [ ":" port ]`, strict dec-octet
|
|
175
|
+
//! (no leading zeros), IPv6 / IPvFuture / pct-encoding.
|
|
176
|
+
const UNRESERVED = /[A-Za-z0-9._~-]/;
|
|
177
|
+
const SUB_DELIM = /[!$&'()*+,;=]/;
|
|
178
|
+
const HEX = /[0-9A-Fa-f]/;
|
|
179
|
+
function isScheme(s) {
|
|
180
|
+
if (s.length === 0 || !/[A-Za-z]/.test(s.charAt(0))) return false;
|
|
181
|
+
for (const c of s) if (!/[A-Za-z0-9+.-]/.test(c)) return false;
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
/** `(unreserved / pct-encoded / sub-delims / ":" / "@")*`, ASCII only. */
|
|
185
|
+
function everyPct(s, allowed) {
|
|
186
|
+
for (let i = 0; i < s.length; i++) {
|
|
187
|
+
const c = s.charAt(i);
|
|
188
|
+
if (c === "%") {
|
|
189
|
+
if (i + 2 >= s.length || !HEX.test(s.charAt(i + 1)) || !HEX.test(s.charAt(i + 2))) return false;
|
|
190
|
+
i += 2;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!allowed(c)) return false;
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
function isUserinfo(s) {
|
|
198
|
+
return everyPct(s, (c) => UNRESERVED.test(c) || SUB_DELIM.test(c) || c === ":");
|
|
199
|
+
}
|
|
200
|
+
function isRegName(s) {
|
|
201
|
+
return everyPct(s, (c) => UNRESERVED.test(c) || SUB_DELIM.test(c));
|
|
202
|
+
}
|
|
203
|
+
function isPcharString(s) {
|
|
204
|
+
return everyPct(s, (c) => UNRESERVED.test(c) || SUB_DELIM.test(c) || c === ":" || c === "@");
|
|
205
|
+
}
|
|
206
|
+
/** Dec-octet: 0-255, no leading zeros (RFC 3986 strict). */
|
|
207
|
+
function isDecOctet(s) {
|
|
208
|
+
if (s.length === 0 || s.length > 3) return false;
|
|
209
|
+
if (s.length > 1 && s.startsWith("0")) return false;
|
|
210
|
+
for (const c of s) if (!/[0-9]/.test(c)) return false;
|
|
211
|
+
return Number(s) <= 255;
|
|
212
|
+
}
|
|
213
|
+
function isIPv4(s) {
|
|
214
|
+
const parts = s.split(".");
|
|
215
|
+
return parts.length === 4 && parts.every(isDecOctet);
|
|
216
|
+
}
|
|
217
|
+
function isH16(s) {
|
|
218
|
+
return s.length > 0 && s.length <= 4 && s.split("").every((c) => HEX.test(c));
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Count h16-equivalent groups in one half of an IPv6 literal (an IPv4 tail counts as two groups and
|
|
222
|
+
* must be last).
|
|
223
|
+
*/
|
|
224
|
+
function ipv6GroupCount(part) {
|
|
225
|
+
if (part === "") return 0;
|
|
226
|
+
let n = 0;
|
|
227
|
+
const groups = part.split(":");
|
|
228
|
+
for (const g of groups) {
|
|
229
|
+
if (g === "") return -1;
|
|
230
|
+
if (g.includes(".")) {
|
|
231
|
+
if (g !== groups.at(-1) || !isIPv4(g)) return -1;
|
|
232
|
+
n += 2;
|
|
233
|
+
} else if (isH16(g)) n += 1;
|
|
234
|
+
else return -1;
|
|
235
|
+
}
|
|
236
|
+
return n;
|
|
237
|
+
}
|
|
238
|
+
/** IPv6address per RFC 3986, with optional IPv4 tail and `::` compression. */
|
|
239
|
+
function isIPv6(s) {
|
|
240
|
+
const halves = s.split("::");
|
|
241
|
+
if (halves.length > 2) return false;
|
|
242
|
+
if (halves.length === 1) return ipv6GroupCount(halves[0] ?? "") === 8;
|
|
243
|
+
const hn = ipv6GroupCount(halves[0] ?? "");
|
|
244
|
+
const tn = ipv6GroupCount(halves[1] ?? "");
|
|
245
|
+
if (hn === -1 || tn === -1) return false;
|
|
246
|
+
return hn + tn < 8;
|
|
247
|
+
}
|
|
248
|
+
function isIPvFuture(s) {
|
|
249
|
+
const m = /^[vV]([0-9A-Fa-f]+)\.(.*)$/.exec(s);
|
|
250
|
+
if (m === null) return false;
|
|
251
|
+
const tail = m[2] ?? "";
|
|
252
|
+
if (tail.length === 0) return false;
|
|
253
|
+
for (const c of tail) if (!(UNRESERVED.test(c) || SUB_DELIM.test(c) || c === ":")) return false;
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
/** `host = IP-literal / IPv4address / reg-name` (first-match-wins). */
|
|
257
|
+
function isHost(s) {
|
|
258
|
+
if (s.startsWith("[")) return s.endsWith("]") && (isIPv6(s.slice(1, -1)) || isIPvFuture(s.slice(1, -1)));
|
|
259
|
+
if (isIPv4(s)) return true;
|
|
260
|
+
return isRegName(s);
|
|
261
|
+
}
|
|
262
|
+
/** `authority = [ userinfo "@" ] host [ ":" port ]`. */
|
|
263
|
+
function isAuthority(s) {
|
|
264
|
+
let rest = s;
|
|
265
|
+
const at = rest.lastIndexOf("@");
|
|
266
|
+
if (at !== -1) {
|
|
267
|
+
if (rest.indexOf("@") !== at) return false;
|
|
268
|
+
if (!isUserinfo(rest.slice(0, at))) return false;
|
|
269
|
+
rest = rest.slice(at + 1);
|
|
270
|
+
}
|
|
271
|
+
let host = rest;
|
|
272
|
+
let port = "";
|
|
273
|
+
if (rest.startsWith("[")) {
|
|
274
|
+
const end = rest.indexOf("]");
|
|
275
|
+
if (end === -1) return false;
|
|
276
|
+
host = rest.slice(0, end + 1);
|
|
277
|
+
const after = rest.slice(end + 1);
|
|
278
|
+
if (after !== "") {
|
|
279
|
+
if (!after.startsWith(":")) return false;
|
|
280
|
+
port = after.slice(1);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
const colon = rest.lastIndexOf(":");
|
|
284
|
+
if (colon !== -1) {
|
|
285
|
+
host = rest.slice(0, colon);
|
|
286
|
+
port = rest.slice(colon + 1);
|
|
287
|
+
if (host.includes(":")) return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (port !== "" && !port.split("").every((c) => /[0-9]/.test(c))) return false;
|
|
291
|
+
return isHost(host);
|
|
292
|
+
}
|
|
293
|
+
/** `path` characters: each segment is `*pchar`. */
|
|
294
|
+
function isPath(s) {
|
|
295
|
+
for (const seg of s.split("/")) if (!isPcharString(seg)) return false;
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
/** `query` / `fragment`: `*( pchar / "/" / "?" )`. */
|
|
299
|
+
function isQueryOrFragment(s) {
|
|
300
|
+
return everyPct(s, (c) => UNRESERVED.test(c) || SUB_DELIM.test(c) || c === ":" || c === "@" || c === "/" || c === "?");
|
|
301
|
+
}
|
|
302
|
+
/** RFC 3986 `URI` (absolute, with scheme). */
|
|
303
|
+
function isUri(s) {
|
|
304
|
+
const hash = s.indexOf("#");
|
|
305
|
+
if (hash !== -1 && s.includes("#", hash + 1)) return false;
|
|
306
|
+
let rest = s;
|
|
307
|
+
let fragment;
|
|
308
|
+
if (hash !== -1) {
|
|
309
|
+
fragment = rest.slice(hash + 1);
|
|
310
|
+
rest = rest.slice(0, hash);
|
|
311
|
+
}
|
|
312
|
+
const q = rest.indexOf("?");
|
|
313
|
+
let query;
|
|
314
|
+
if (q !== -1) {
|
|
315
|
+
query = rest.slice(q + 1);
|
|
316
|
+
rest = rest.slice(0, q);
|
|
317
|
+
}
|
|
318
|
+
const colon = rest.indexOf(":");
|
|
319
|
+
if (colon === -1) return false;
|
|
320
|
+
if (!isScheme(rest.slice(0, colon))) return false;
|
|
321
|
+
let hier = rest.slice(colon + 1);
|
|
322
|
+
if (hier.startsWith("//")) {
|
|
323
|
+
hier = hier.slice(2);
|
|
324
|
+
const slash = hier.indexOf("/");
|
|
325
|
+
const authority = slash === -1 ? hier : hier.slice(0, slash);
|
|
326
|
+
const path = slash === -1 ? "" : hier.slice(slash);
|
|
327
|
+
if (!isAuthority(authority)) return false;
|
|
328
|
+
if (!isPath(path)) return false;
|
|
329
|
+
} else if (!isPath(hier)) return false;
|
|
330
|
+
if (query !== void 0 && !isQueryOrFragment(query)) return false;
|
|
331
|
+
if (fragment !== void 0 && !isQueryOrFragment(fragment)) return false;
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/core/timestamp.ts
|
|
336
|
+
//! Lexical RFC 3339 `date-time` + instant, mirroring Rust `Timestamp`.
|
|
337
|
+
const SHAPE = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
338
|
+
/** Days since 1970-01-01 for a proleptic-Gregorian y/m/d (Hinnant's algorithm). */
|
|
339
|
+
function daysFromCivil(y, m, d) {
|
|
340
|
+
const yy = m <= 2 ? y - 1 : y;
|
|
341
|
+
const era = Math.floor((yy >= 0 ? yy : yy - 399) / 400);
|
|
342
|
+
const yoe = yy - era * 400;
|
|
343
|
+
const doy = Math.floor((153 * (m > 2 ? m - 3 : m + 9) + 2) / 5) + d - 1;
|
|
344
|
+
const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
|
|
345
|
+
return era * 146097 + doe - 719468;
|
|
346
|
+
}
|
|
347
|
+
function daysInMonth(year, month) {
|
|
348
|
+
return [
|
|
349
|
+
31,
|
|
350
|
+
year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
|
|
351
|
+
31,
|
|
352
|
+
30,
|
|
353
|
+
31,
|
|
354
|
+
30,
|
|
355
|
+
31,
|
|
356
|
+
31,
|
|
357
|
+
30,
|
|
358
|
+
31,
|
|
359
|
+
30,
|
|
360
|
+
31
|
|
361
|
+
][month - 1] ?? 0;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Parse an RFC 3339 `date-time`; `undefined` when invalid.
|
|
365
|
+
*
|
|
366
|
+
* Requires `T`/`t` and `Z`/`z`/`±HH:MM`; optional fraction; real calendar validity (Feb 30
|
|
367
|
+
* rejected); leap second `:60` rejected like Rust `time`.
|
|
368
|
+
*/
|
|
369
|
+
function parseTimestamp(s) {
|
|
370
|
+
const m = SHAPE.exec(s);
|
|
371
|
+
if (m === null) return;
|
|
372
|
+
const [, ys, mos, ds, hs, mis, ss, frac, tz] = m;
|
|
373
|
+
const year = Number(ys);
|
|
374
|
+
const month = Number(mos);
|
|
375
|
+
const day = Number(ds);
|
|
376
|
+
const hour = Number(hs);
|
|
377
|
+
const minute = Number(mis);
|
|
378
|
+
const second = Number(ss);
|
|
379
|
+
if (month < 1 || month > 12) return;
|
|
380
|
+
if (day < 1 || day > daysInMonth(year, month)) return;
|
|
381
|
+
if (hour > 23 || minute > 59 || second > 59) return;
|
|
382
|
+
let ms = daysFromCivil(year, month, day) * 864e5 + hour * 36e5 + minute * 6e4 + second * 1e3;
|
|
383
|
+
if (frac !== void 0 && frac.length > 1) ms += Number(frac.slice(1, 4).padEnd(3, "0"));
|
|
384
|
+
if (tz !== void 0 && tz !== "Z" && tz !== "z") {
|
|
385
|
+
const oh = Number(tz.slice(1, 3));
|
|
386
|
+
const om = Number(tz.slice(4, 6));
|
|
387
|
+
if (oh > 23 || om > 59) return;
|
|
388
|
+
const off = (oh * 60 + om) * 6e4;
|
|
389
|
+
ms += tz.startsWith("+") ? -off : off;
|
|
390
|
+
}
|
|
391
|
+
if (!Number.isFinite(ms)) return;
|
|
392
|
+
return {
|
|
393
|
+
lexeme: s,
|
|
394
|
+
epochMs: Math.trunc(ms)
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* `Date.prototype.toISOString()` semantics: UTC, millisecond precision, `YYYY-MM-DDTHH:MM:SS.sssZ`;
|
|
399
|
+
* `undefined` for invalid dates and years outside `0000..=9999`.
|
|
400
|
+
*/
|
|
401
|
+
function timestampFromDate(date) {
|
|
402
|
+
const ms = date.getTime();
|
|
403
|
+
if (!Number.isFinite(ms)) return;
|
|
404
|
+
const year = date.getUTCFullYear();
|
|
405
|
+
if (year < 0 || year > 9999) return;
|
|
406
|
+
try {
|
|
407
|
+
return date.toISOString();
|
|
408
|
+
} catch {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/core/utf8.ts
|
|
414
|
+
//! Strict UTF-8 helpers.
|
|
415
|
+
//!
|
|
416
|
+
//! `TextEncoder`/`TextDecoder` exist on every supported platform (Node,
|
|
417
|
+
//! browsers, Hermes). Decoding must reject exactly what Rust
|
|
418
|
+
//! `str::from_utf8` rejects — overlongs, surrogates, > U+10FFFF,
|
|
419
|
+
//! truncated sequences — so `fatal: true` is used (which follows the
|
|
420
|
+
//! WHATWG fatal algorithm and rejects all of those).
|
|
421
|
+
const encoder = new TextEncoder();
|
|
422
|
+
const fatalDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
423
|
+
/** Encode `text` as UTF-8 bytes. */
|
|
424
|
+
function utf8Encode(text) {
|
|
425
|
+
return encoder.encode(text);
|
|
426
|
+
}
|
|
427
|
+
/** Decode `bytes` as strict UTF-8; `undefined` on any invalid sequence. */
|
|
428
|
+
function utf8Decode(bytes) {
|
|
429
|
+
try {
|
|
430
|
+
return fatalDecoder.decode(bytes);
|
|
431
|
+
} catch {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/** UTF-8 byte length of `text` (JS string length counts UTF-16 code units). */
|
|
436
|
+
function utf8ByteLength(text) {
|
|
437
|
+
return encoder.encode(text).length;
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/core/message.ts
|
|
441
|
+
//! CAIP-122 abstract data model: lexical `MessageFields` and the
|
|
442
|
+
//! validated `Message` produced by `createMessage` (single validation
|
|
443
|
+
//! path, same check order as Rust `Message::new`).
|
|
444
|
+
const STATEMENT_CHARS = /* @__PURE__ */ new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;= ");
|
|
445
|
+
function isCaip10Address(s) {
|
|
446
|
+
if (s.length === 0 || s.length > 128) return false;
|
|
447
|
+
for (const c of s) if (!/[A-Za-z0-9]/.test(c) && c !== "-" && c !== "." && c !== "%") return false;
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Validate `fields` and return a frozen [`Message`].
|
|
452
|
+
*
|
|
453
|
+
* Check order (as Rust `Message::new`): scheme, domain, address, statement, uri, version, chainId,
|
|
454
|
+
* nonce, issuedAt, expirationTime, notBefore, requestId, resources.
|
|
455
|
+
*
|
|
456
|
+
* @throws {SiwxError} `InvalidField` naming the first invalid field.
|
|
457
|
+
*/
|
|
458
|
+
function createMessage(fields) {
|
|
459
|
+
if (fields.scheme !== void 0 && !isScheme(fields.scheme)) throw fieldError("scheme", "not an RFC 3986 scheme");
|
|
460
|
+
if (fields.domain.length === 0) throw fieldError("domain", "empty");
|
|
461
|
+
if (fields.domain.includes("@")) throw fieldError("domain", "userinfo not allowed");
|
|
462
|
+
if (fields.domain.startsWith(":")) throw fieldError("domain", "empty host");
|
|
463
|
+
if (!isAuthority(fields.domain)) throw fieldError("domain", "not an RFC 3986 authority");
|
|
464
|
+
if (!isCaip10Address(fields.address)) throw fieldError("address", fields.address.length === 0 || fields.address.length > 128 ? "expected 1..=128 characters" : "bad CAIP-10 charset");
|
|
465
|
+
if (fields.statement !== void 0) {
|
|
466
|
+
if (utf8ByteLength(fields.statement) > 4096) throw fieldError("statement", "exceeds maximum size");
|
|
467
|
+
for (const c of fields.statement) if (!STATEMENT_CHARS.has(c)) throw fieldError("statement", "must be RFC 3986 reserved / unreserved / SP");
|
|
468
|
+
}
|
|
469
|
+
checkUri(fields.uri, "uri");
|
|
470
|
+
if (fields.version !== "1") throw fieldError("version", "must be \"1\"");
|
|
471
|
+
if (fields.chainId.length === 0) throw fieldError("chainId", "empty");
|
|
472
|
+
if (fields.chainId.length > 64) throw fieldError("chainId", "exceeds 64 characters");
|
|
473
|
+
for (const c of fields.chainId) if (!/[A-Za-z0-9]/.test(c) && c !== "-" && c !== "_" && c !== ":") throw fieldError("chainId", "bad charset");
|
|
474
|
+
if (fields.nonce.length < 8) throw fieldError("nonce", "too short");
|
|
475
|
+
for (const c of fields.nonce) if (!/[A-Za-z0-9]/.test(c)) throw fieldError("nonce", "not ASCII alphanumeric");
|
|
476
|
+
if (parseTimestamp(fields.issuedAt) === void 0) throw fieldError("issuedAt", "not an RFC 3339 date-time");
|
|
477
|
+
if (fields.expirationTime !== void 0 && parseTimestamp(fields.expirationTime) === void 0) throw fieldError("expirationTime", "not an RFC 3339 date-time");
|
|
478
|
+
if (fields.notBefore !== void 0 && parseTimestamp(fields.notBefore) === void 0) throw fieldError("notBefore", "not an RFC 3339 date-time");
|
|
479
|
+
if (fields.requestId !== void 0) {
|
|
480
|
+
if (utf8ByteLength(fields.requestId) > 128) throw fieldError("requestId", "exceeds maximum size");
|
|
481
|
+
if (!isPcharString(fields.requestId)) throw fieldError("requestId", "must be RFC 3986 *pchar");
|
|
482
|
+
}
|
|
483
|
+
if (fields.resources !== void 0) {
|
|
484
|
+
if (fields.resources.length > 32) throw fieldError("resources", "too many");
|
|
485
|
+
for (const uri of fields.resources) checkUri(uri, "resources");
|
|
486
|
+
}
|
|
487
|
+
const out = {
|
|
488
|
+
domain: fields.domain,
|
|
489
|
+
address: fields.address,
|
|
490
|
+
uri: fields.uri,
|
|
491
|
+
version: fields.version,
|
|
492
|
+
chainId: fields.chainId,
|
|
493
|
+
nonce: fields.nonce,
|
|
494
|
+
issuedAt: fields.issuedAt,
|
|
495
|
+
...fields.scheme === void 0 ? {} : { scheme: fields.scheme },
|
|
496
|
+
...fields.statement === void 0 ? {} : { statement: fields.statement },
|
|
497
|
+
...fields.expirationTime === void 0 ? {} : { expirationTime: fields.expirationTime },
|
|
498
|
+
...fields.notBefore === void 0 ? {} : { notBefore: fields.notBefore },
|
|
499
|
+
...fields.requestId === void 0 ? {} : { requestId: fields.requestId },
|
|
500
|
+
...fields.resources === void 0 ? {} : { resources: fields.resources }
|
|
501
|
+
};
|
|
502
|
+
return Object.freeze(out);
|
|
503
|
+
}
|
|
504
|
+
function checkUri(uri, field) {
|
|
505
|
+
if (uri.length === 0) throw fieldError(field, "empty");
|
|
506
|
+
if (utf8ByteLength(uri) > 2048) throw fieldError(field, "exceeds maximum size");
|
|
507
|
+
if (!isUri(uri)) throw fieldError(field, "not an RFC 3986 URI");
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
export { parseAccountId as _, parseTimestamp as a, mismatchError as b, MAX_REQUEST_ID_BYTES as c, MAX_URI_BYTES as d, MIN_NONCE_LEN as f, createAccountId as g, optionalTags as h, utf8Encode as i, MAX_RESOURCES as l, SUFFIX as m, utf8ByteLength as n, timestampFromDate as o, MID as p, utf8Decode as r, MAX_MESSAGE_BYTES as s, createMessage as t, MAX_STATEMENT_BYTES as u, SiwxError as v, syntaxError as x, fieldError as y };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { a as parseTimestamp, b as mismatchError, i as utf8Encode, v as SiwxError } from "./message-ZewmOzps.mjs";
|
|
2
|
+
//#region src/core/expect.ts
|
|
3
|
+
//! Server-side binding + time policy, mirroring Rust `Expect`.
|
|
4
|
+
const DEFAULT_SKEW_SECONDS = 60;
|
|
5
|
+
/**
|
|
6
|
+
* Check `message` against the bindings and time policy.
|
|
7
|
+
*
|
|
8
|
+
* @throws {RangeError} For invalid `time`, or non-finite/negative `skewSeconds`/`maxAgeSeconds`.
|
|
9
|
+
* Order: domain, nonce, scheme, uri, chainId, requestId (`Mismatch`), then `Expired`,
|
|
10
|
+
* `NotYetValid`, `IssuedAtTooOld`. All comparisons at millisecond precision; a future `issued-at`
|
|
11
|
+
* is not an error.
|
|
12
|
+
* @throws {SiwxError} The first failing check.
|
|
13
|
+
*/
|
|
14
|
+
const assertNonNegativeFinite = (name, v) => {
|
|
15
|
+
if (v !== void 0 && (!Number.isFinite(v) || v < 0)) throw new RangeError(`${name} must be a non-negative finite number`);
|
|
16
|
+
};
|
|
17
|
+
/** EpochMs of a timestamp already validated at `Message::new` time. */
|
|
18
|
+
const lexemeMs = (ts) => {
|
|
19
|
+
const parsed = parseTimestamp(ts);
|
|
20
|
+
if (parsed === void 0) throw new RangeError("invalid stored timestamp");
|
|
21
|
+
return parsed.epochMs;
|
|
22
|
+
};
|
|
23
|
+
function checkExpect(message, expect) {
|
|
24
|
+
if (expect.time !== void 0 && !Number.isFinite(expect.time.getTime())) throw new RangeError("expect.time must be a valid Date");
|
|
25
|
+
assertNonNegativeFinite("skewSeconds", expect.skewSeconds);
|
|
26
|
+
assertNonNegativeFinite("maxAgeSeconds", expect.maxAgeSeconds);
|
|
27
|
+
if (message.domain !== expect.domain) throw mismatchError("domain", expect.domain, message.domain);
|
|
28
|
+
if (message.nonce !== expect.nonce) throw mismatchError("nonce", expect.nonce, message.nonce);
|
|
29
|
+
if (expect.scheme !== void 0 && message.scheme !== expect.scheme) throw mismatchError("scheme", expect.scheme, message.scheme);
|
|
30
|
+
if (expect.uri !== void 0 && message.uri !== expect.uri) throw mismatchError("uri", expect.uri, message.uri);
|
|
31
|
+
if (expect.chainId !== void 0 && message.chainId !== expect.chainId) throw mismatchError("chainId", expect.chainId, message.chainId);
|
|
32
|
+
if (expect.requestId !== void 0 && message.requestId !== expect.requestId) throw mismatchError("requestId", expect.requestId, message.requestId);
|
|
33
|
+
const now = Math.floor((expect.time ?? /* @__PURE__ */ new Date()).getTime());
|
|
34
|
+
const skew = Math.floor((expect.skewSeconds ?? DEFAULT_SKEW_SECONDS) * 1e3);
|
|
35
|
+
if (message.expirationTime !== void 0 && now > lexemeMs(message.expirationTime) + skew) throw new SiwxError({ code: "Expired" });
|
|
36
|
+
if (message.notBefore !== void 0 && now + skew < lexemeMs(message.notBefore)) throw new SiwxError({ code: "NotYetValid" });
|
|
37
|
+
if (expect.maxAgeSeconds !== void 0) {
|
|
38
|
+
const maxAge = Math.floor(expect.maxAgeSeconds * 1e3);
|
|
39
|
+
if (now > lexemeMs(message.issuedAt) + maxAge + skew) throw new SiwxError({ code: "IssuedAtTooOld" });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/core/namespace.ts
|
|
44
|
+
//! Chain namespace abstraction: parsing, formatting, and signature
|
|
45
|
+
//! verification for one CAIP-2 namespace.
|
|
46
|
+
/**
|
|
47
|
+
* Run a synchronous verification body as a promise; throws reject. Shared idiom for namespace
|
|
48
|
+
* `verify` implementations.
|
|
49
|
+
*/
|
|
50
|
+
async function settle(run) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
resolve(run());
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Full backend login flow: parse → checkExpect → account → verify. */
|
|
56
|
+
async function authenticate(namespace, input) {
|
|
57
|
+
const signed = typeof input.message === "string" ? utf8Encode(input.message) : input.message;
|
|
58
|
+
const message = namespace.parse(signed);
|
|
59
|
+
checkExpect(message, input.expect);
|
|
60
|
+
const account = namespace.account(message);
|
|
61
|
+
await namespace.verify(message, signed, input.signature);
|
|
62
|
+
return {
|
|
63
|
+
message,
|
|
64
|
+
account
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { settle as n, checkExpect as r, authenticate as t };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//#region src/core/account.d.ts
|
|
2
|
+
/** A validated CAIP-10 `account_id`. */
|
|
3
|
+
type AccountId = {
|
|
4
|
+
readonly namespace: string;
|
|
5
|
+
readonly reference: string;
|
|
6
|
+
readonly address: string;
|
|
7
|
+
/** Canonical string form `namespace:reference:address`. */
|
|
8
|
+
readonly id: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Validate and build an `account_id`.
|
|
12
|
+
*
|
|
13
|
+
* `namespace` is CAIP-2 `[-a-z0-9]{3,8}`; `reference` is `[-_a-zA-Z0-9]{1,32}`; `address` is
|
|
14
|
+
* `[-.%a-zA-Z0-9]{1,128}`.
|
|
15
|
+
*
|
|
16
|
+
* @throws {SiwxError} `InvalidField` `chainId` for bad namespace/reference, `address` for a bad
|
|
17
|
+
* address.
|
|
18
|
+
*/
|
|
19
|
+
declare function createAccountId(namespace: string, reference: string, address: string): AccountId;
|
|
20
|
+
/**
|
|
21
|
+
* Parse `namespace:reference:address`.
|
|
22
|
+
*
|
|
23
|
+
* @throws {SiwxError} `InvalidField` `chainId` when the shape is wrong.
|
|
24
|
+
*/
|
|
25
|
+
declare function parseAccountId(id: string): AccountId;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/core/message.d.ts
|
|
28
|
+
/** Plain lexical form of a CAIP-122 message (all strings, absent = `undefined`). */
|
|
29
|
+
type MessageFields = {
|
|
30
|
+
readonly scheme?: string | undefined;
|
|
31
|
+
readonly domain: string;
|
|
32
|
+
readonly address: string;
|
|
33
|
+
readonly statement?: string | undefined;
|
|
34
|
+
readonly uri: string;
|
|
35
|
+
readonly version: string;
|
|
36
|
+
readonly chainId: string;
|
|
37
|
+
readonly nonce: string;
|
|
38
|
+
readonly issuedAt: string;
|
|
39
|
+
readonly expirationTime?: string | undefined;
|
|
40
|
+
readonly notBefore?: string | undefined;
|
|
41
|
+
readonly requestId?: string | undefined;
|
|
42
|
+
readonly resources?: ReadonlyArray<string> | undefined;
|
|
43
|
+
};
|
|
44
|
+
declare const brand: unique symbol;
|
|
45
|
+
/** Validated CAIP-122 message; construct only via `createMessage`. */
|
|
46
|
+
type Message = {
|
|
47
|
+
readonly [brand]: "Message";
|
|
48
|
+
} & MessageFields;
|
|
49
|
+
/**
|
|
50
|
+
* Validate `fields` and return a frozen [`Message`].
|
|
51
|
+
*
|
|
52
|
+
* Check order (as Rust `Message::new`): scheme, domain, address, statement, uri, version, chainId,
|
|
53
|
+
* nonce, issuedAt, expirationTime, notBefore, requestId, resources.
|
|
54
|
+
*
|
|
55
|
+
* @throws {SiwxError} `InvalidField` naming the first invalid field.
|
|
56
|
+
*/
|
|
57
|
+
declare function createMessage(fields: MessageFields): Message;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/core/expect.d.ts
|
|
60
|
+
/** Expected bindings and clock policy for `checkExpect`. */
|
|
61
|
+
type Expect = {
|
|
62
|
+
readonly domain: string;
|
|
63
|
+
readonly nonce: string;
|
|
64
|
+
readonly scheme?: string | undefined;
|
|
65
|
+
readonly uri?: string | undefined;
|
|
66
|
+
readonly chainId?: string | undefined;
|
|
67
|
+
readonly requestId?: string | undefined;
|
|
68
|
+
/** Reference instant (defaults to `new Date()`). */
|
|
69
|
+
readonly time?: Date | undefined;
|
|
70
|
+
/** Clock-skew tolerance in seconds (default 60). */
|
|
71
|
+
readonly skewSeconds?: number | undefined;
|
|
72
|
+
/** Maximum age of `issued-at` in seconds. */
|
|
73
|
+
readonly maxAgeSeconds?: number | undefined;
|
|
74
|
+
};
|
|
75
|
+
declare function checkExpect(message: Message, expect: Expect): void;
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/core/namespace.d.ts
|
|
78
|
+
/** A chain namespace profile. */
|
|
79
|
+
type Namespace = {
|
|
80
|
+
/** CAIP-2 namespace, e.g. `"eip155"` / `"solana"`. */
|
|
81
|
+
readonly namespace: string;
|
|
82
|
+
/** Preamble label, e.g. `"Ethereum"` / `"Solana"`. */
|
|
83
|
+
readonly label: string;
|
|
84
|
+
/** Map `message` to an `AccountId` under this namespace's rules. */
|
|
85
|
+
account: (message: Message) => AccountId;
|
|
86
|
+
/** Render `message` to this namespace's signing text. */
|
|
87
|
+
format: (message: Message) => string;
|
|
88
|
+
/** Parse `signed` bytes, requiring label === `this.label`. */
|
|
89
|
+
parse: (signed: Uint8Array) => Message;
|
|
90
|
+
/** Verify `signature` over the exact `signed` bytes. */
|
|
91
|
+
verify: (message: Message, signed: Uint8Array, signature: Uint8Array) => Promise<void>;
|
|
92
|
+
};
|
|
93
|
+
/** Successful `authenticate`: parsed message plus CAIP-10 identity. */
|
|
94
|
+
type Authenticated = {
|
|
95
|
+
readonly message: Message;
|
|
96
|
+
readonly account: AccountId;
|
|
97
|
+
};
|
|
98
|
+
/** Full backend login flow: parse → checkExpect → account → verify. */
|
|
99
|
+
declare function authenticate(namespace: Namespace, input: {
|
|
100
|
+
readonly message: string | Uint8Array;
|
|
101
|
+
readonly signature: Uint8Array;
|
|
102
|
+
readonly expect: Expect;
|
|
103
|
+
}): Promise<Authenticated>;
|
|
104
|
+
//#endregion
|
|
105
|
+
export { checkExpect as a, createMessage as c, parseAccountId as d, Expect as i, AccountId as l, Namespace as n, Message as o, authenticate as r, MessageFields as s, Authenticated as t, createAccountId as u };
|