@qntx/siwx 0.0.0-stage → 0.7.0

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.
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
package/dist/svm.d.mts ADDED
@@ -0,0 +1,42 @@
1
+ import { n as Namespace, o as Message } from "./namespace-DlgXqLsI.mjs";
2
+ //#region src/svm/address.d.ts
3
+ /**
4
+ * Validate that `address` is a canonical, non-identity, prime-order-subgroup Ed25519 verifying key
5
+ * in base58 form.
6
+ *
7
+ * @throws {SiwxError} `InvalidField` `address`.
8
+ */
9
+ export declare function validateAddress(address: string): void;
10
+ declare namespace siws_d_exports {
11
+ export { format, parse, parseBytes };
12
+ }
13
+ /**
14
+ * Format `message` as a SIWS signing text.
15
+ *
16
+ * @throws {SiwxError} `InvalidField` `scheme` (SIWS has none) or `statement` (empty).
17
+ */
18
+ declare function format(message: Message): string;
19
+ /**
20
+ * Parse `text` into a validated message; the preamble label must be exactly `"Solana"`.
21
+ *
22
+ * @throws {SiwxError} `Syntax`, `Mismatch` `label`, or `InvalidField`.
23
+ */
24
+ declare function parse(text: string): Message;
25
+ /**
26
+ * {@link parse} over raw bytes: size check, strict UTF-8 (`InvalidUtf8`), then parse.
27
+ *
28
+ * @throws {SiwxError}
29
+ */
30
+ declare function parseBytes(bytes: Uint8Array): Message;
31
+ //#endregion
32
+ //#region src/svm/index.d.ts
33
+ /**
34
+ * Wrap `text` in the off-chain message v1 envelope for `signer` (validated as an SIWS address).
35
+ *
36
+ * @throws {SiwxError} `InvalidField` `address`.
37
+ */
38
+ export declare function offchainMessage(text: string, signer: string): Uint8Array;
39
+ /** Create the Solana namespace profile. */
40
+ export declare function svm(): Namespace;
41
+ //#endregion
42
+ export { siws_d_exports as siws };
package/dist/svm.mjs ADDED
@@ -0,0 +1,329 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { g as createAccountId, h as optionalTags, i as utf8Encode, m as SUFFIX, n as utf8ByteLength, p as MID, r as utf8Decode, s as MAX_MESSAGE_BYTES, t as createMessage, v as SiwxError, x as syntaxError, y as fieldError } from "./message-ZewmOzps.mjs";
3
+ import { n as settle } from "./namespace-Cl7PJYC-.mjs";
4
+ import { bytesToNumberLE, equalBytes } from "@noble/curves/utils.js";
5
+ import { ed25519 } from "@noble/curves/ed25519.js";
6
+ import { base58 } from "@scure/base";
7
+ //#region src/svm/address.ts
8
+ /** Solana address rules: canonical on-curve Ed25519 key in base58. */
9
+ /**
10
+ * Decode `address` as a base58 (Bitcoin alphabet) string of 32–44 chars that decodes to exactly 32
11
+ * bytes.
12
+ *
13
+ * @throws {SiwxError} `InvalidField` `address`.
14
+ */
15
+ function addressBytes(address) {
16
+ if (address.length < 32 || address.length > 44) throw fieldError("address", "expected 32–44 base58 chars");
17
+ let bytes;
18
+ try {
19
+ bytes = base58.decode(address);
20
+ } catch {
21
+ throw fieldError("address", "invalid base58");
22
+ }
23
+ if (bytes.length !== 32) throw fieldError("address", "expected 32 bytes");
24
+ return bytes;
25
+ }
26
+ /**
27
+ * The canonical, non-identity, prime-order-subgroup Ed25519 point for `bytes` (decompress →
28
+ * recompress byte equality proves canonicity).
29
+ *
30
+ * @throws {SiwxError} `InvalidField` `address`.
31
+ */
32
+ function canonicalPoint(bytes) {
33
+ let point;
34
+ try {
35
+ point = ed25519.Point.fromBytes(bytes);
36
+ } catch {
37
+ throw fieldError("address", "invalid Ed25519 pubkey");
38
+ }
39
+ if (point.equals(ed25519.Point.ZERO)) throw fieldError("address", "identity pubkey");
40
+ if (!point.isTorsionFree()) throw fieldError("address", "not in the prime-order subgroup");
41
+ if (!equalBytes(point.toBytes(), bytes)) throw fieldError("address", "non-canonical pubkey");
42
+ }
43
+ /**
44
+ * Validate that `address` is a canonical, non-identity, prime-order-subgroup Ed25519 verifying key
45
+ * in base58 form.
46
+ *
47
+ * @throws {SiwxError} `InvalidField` `address`.
48
+ */
49
+ function validateAddress(address) {
50
+ canonicalPoint(addressBytes(address));
51
+ }
52
+ /** Base58-encode `bytes` (error messages). */
53
+ function b58(bytes) {
54
+ return base58.encode(bytes);
55
+ }
56
+ //#endregion
57
+ //#region src/svm/ed25519.ts
58
+ /** Ed25519 signature verification for Solana (internal). */
59
+ const L = ed25519.Point.CURVE().n;
60
+ const P = ed25519.Point;
61
+ /**
62
+ * RFC 8032 verification over the exact `signed` bytes.
63
+ *
64
+ * `A` (the address key) and `R` must be canonical encodings of prime-order-subgroup points
65
+ * (torsion-free — excluding identity, small-order and mixed points) and `S < L`; for torsion-free
66
+ * `A`/`R` the cofactored and cofactorless equations coincide, so library verifiers agree.
67
+ *
68
+ * @throws {SiwxError} `InvalidSignature` (wrong length or `S >= L`) or `VerificationFailed` (bad
69
+ * `R`, or the equation fails).
70
+ */
71
+ function verifyEddsa(message, signed, signature) {
72
+ if (signature.length !== 64) throw new SiwxError({
73
+ code: "InvalidSignature",
74
+ reason: "Ed25519 signature must be 64 bytes"
75
+ });
76
+ if (bytesToNumberLE(signature.subarray(32, 64)) >= L) throw new SiwxError({
77
+ code: "InvalidSignature",
78
+ reason: "S must be a canonical scalar (S < L)"
79
+ });
80
+ const key = addressBytes(message.address);
81
+ canonicalPoint(key);
82
+ const rBytes = signature.subarray(0, 32);
83
+ let r;
84
+ try {
85
+ r = P.fromBytes(rBytes);
86
+ } catch {
87
+ throw new SiwxError({ code: "VerificationFailed" });
88
+ }
89
+ if (!equalBytes(r.toBytes(), rBytes) || !r.isTorsionFree() || r.equals(P.ZERO)) throw new SiwxError({ code: "VerificationFailed" });
90
+ let ok;
91
+ try {
92
+ ok = ed25519.verify(signature, signed, key, { zip215: false });
93
+ } catch {
94
+ throw new SiwxError({ code: "VerificationFailed" });
95
+ }
96
+ if (!ok) throw new SiwxError({ code: "VerificationFailed" });
97
+ }
98
+ //#endregion
99
+ //#region src/svm/siws.ts
100
+ /** SIWS (Sign-In-With-Solana) text layout — Phantom ABNF, LF only. */
101
+ var siws_exports = /* @__PURE__ */ __exportAll({
102
+ format: () => format,
103
+ parse: () => parse,
104
+ parseBytes: () => parseBytes
105
+ });
106
+ const LABEL = "Solana";
107
+ /**
108
+ * Format `message` as a SIWS signing text.
109
+ *
110
+ * @throws {SiwxError} `InvalidField` `scheme` (SIWS has none) or `statement` (empty).
111
+ */
112
+ function format(message) {
113
+ if (message.scheme !== void 0) throw new SiwxError({
114
+ code: "InvalidField",
115
+ field: "scheme",
116
+ reason: "not supported by SIWS"
117
+ });
118
+ if (message.statement !== void 0) {
119
+ if (message.statement === "") throw new SiwxError({
120
+ code: "InvalidField",
121
+ field: "statement",
122
+ reason: "empty"
123
+ });
124
+ if (message.statement.startsWith("URI: ")) throw new SiwxError({
125
+ code: "InvalidField",
126
+ field: "statement",
127
+ reason: "ambiguous with the URI: tag"
128
+ });
129
+ }
130
+ let out = `${message.domain}${MID}${LABEL}${SUFFIX}\n${message.address}`;
131
+ if (message.statement !== void 0) out += `\n\n${message.statement}`;
132
+ out += `\n\nURI: ${message.uri}`;
133
+ out += `\nVersion: ${message.version}`;
134
+ out += `\nChain ID: ${message.chainId}`;
135
+ out += `\nNonce: ${message.nonce}`;
136
+ out += `\nIssued At: ${message.issuedAt}`;
137
+ if (message.expirationTime !== void 0) out += `\nExpiration Time: ${message.expirationTime}`;
138
+ if (message.notBefore !== void 0) out += `\nNot Before: ${message.notBefore}`;
139
+ if (message.requestId !== void 0) out += `\nRequest ID: ${message.requestId}`;
140
+ if (message.resources !== void 0) {
141
+ out += "\nResources:";
142
+ for (const r of message.resources) out += `\n- ${r}`;
143
+ }
144
+ return out;
145
+ }
146
+ const missingUri = () => syntaxError("MissingField", "uri");
147
+ /** The optional statement and the gap up to the required `URI: ` line. */
148
+ function statementThenFields(lines, i) {
149
+ if (i.v >= lines.length) throw missingUri();
150
+ if (lines[i.v] !== "") throw syntaxError("ExpectedBlankLine");
151
+ i.v++;
152
+ const l = lines[i.v];
153
+ if (l === void 0) throw missingUri();
154
+ if (l === "") throw syntaxError("ExpectedBlankLine");
155
+ if (l.startsWith("URI: ")) return;
156
+ i.v++;
157
+ const after = lines[i.v];
158
+ if (after === void 0) throw missingUri();
159
+ if (after !== "") throw syntaxError("ExpectedBlankLine");
160
+ i.v++;
161
+ return l;
162
+ }
163
+ /**
164
+ * Parse `text` into a validated message; the preamble label must be exactly `"Solana"`.
165
+ *
166
+ * @throws {SiwxError} `Syntax`, `Mismatch` `label`, or `InvalidField`.
167
+ */
168
+ function parse(text) {
169
+ const byteLength = utf8ByteLength(text);
170
+ if (byteLength > 16384) throw new SiwxError({
171
+ code: "MessageTooLarge",
172
+ length: byteLength,
173
+ max: MAX_MESSAGE_BYTES
174
+ });
175
+ if (text.includes("\r")) throw syntaxError("CarriageReturn");
176
+ if (text.endsWith("\n")) throw syntaxError("UnexpectedTrailing");
177
+ const lines = text.split("\n");
178
+ const idx = { v: 0 };
179
+ const preamble = lines[idx.v++];
180
+ if (preamble === void 0) throw syntaxError("UnexpectedEof");
181
+ const mid = preamble.indexOf(MID);
182
+ if (mid === -1) throw syntaxError("MissingPreamble");
183
+ const domain = preamble.slice(0, mid);
184
+ const tail = preamble.slice(mid + MID.length);
185
+ if (!tail.endsWith(" account:")) throw syntaxError("MissingAccountSuffix");
186
+ const label = tail.slice(0, tail.length - SUFFIX.length);
187
+ if (label !== LABEL) throw new SiwxError({
188
+ code: "Mismatch",
189
+ field: "label",
190
+ expected: LABEL,
191
+ actual: label
192
+ });
193
+ const address = lines[idx.v++];
194
+ if (address === void 0) throw syntaxError("UnexpectedEof");
195
+ const statement = statementThenFields(lines, idx);
196
+ const take = (tag, field) => {
197
+ const line = lines[idx.v];
198
+ if (line === void 0 || !line.startsWith(tag)) throw syntaxError("MissingField", field);
199
+ idx.v++;
200
+ return line.slice(tag.length);
201
+ };
202
+ const uri = take("URI: ", "uri");
203
+ const version = take("Version: ", "version");
204
+ const chainId = take("Chain ID: ", "chainId");
205
+ const nonce = take("Nonce: ", "nonce");
206
+ const issuedAt = take("Issued At: ", "issuedAt");
207
+ const tail4 = optionalTags(lines, idx.v);
208
+ return createMessage({
209
+ domain,
210
+ address,
211
+ ...statement === void 0 ? {} : { statement },
212
+ uri,
213
+ version,
214
+ chainId,
215
+ nonce,
216
+ issuedAt,
217
+ ...tail4
218
+ });
219
+ }
220
+ /**
221
+ * {@link parse} over raw bytes: size check, strict UTF-8 (`InvalidUtf8`), then parse.
222
+ *
223
+ * @throws {SiwxError}
224
+ */
225
+ function parseBytes(bytes) {
226
+ if (bytes.length > 16384) throw new SiwxError({
227
+ code: "MessageTooLarge",
228
+ length: bytes.length,
229
+ max: MAX_MESSAGE_BYTES
230
+ });
231
+ const text = utf8Decode(bytes);
232
+ if (text === void 0) throw syntaxError("InvalidUtf8");
233
+ return parse(text);
234
+ }
235
+ //#endregion
236
+ //#region src/svm/index.ts
237
+ /**
238
+ * `siwx` Solana (`solana`) namespace profile — SIWS layout plus the off-chain message v1 envelope
239
+ * (sRFC 38).
240
+ */
241
+ const NAMESPACE = "solana";
242
+ const CHAIN_NAME = "Solana";
243
+ const OFFCHAIN_DOMAIN = new Uint8Array([255, ...utf8Encode("solana offchain")]);
244
+ /** CAIP-2 reference for a Solana `chainId` alias. */
245
+ function chainReference(chainId) {
246
+ switch (chainId.startsWith("solana:") ? chainId.slice(7) : chainId) {
247
+ case "mainnet": return "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
248
+ case "testnet": return "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z";
249
+ case "devnet": return "EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
250
+ case "localnet": return "localnet";
251
+ default: throw fieldError("chainId", "unknown Solana cluster");
252
+ }
253
+ }
254
+ function invalidEnvelope() {
255
+ return syntaxError("InvalidEnvelope");
256
+ }
257
+ /** Decode the v1 envelope at the start of `signed`. */
258
+ function decodeEnvelope(signed) {
259
+ if (signed.length < 18) throw invalidEnvelope();
260
+ if ((signed[16] ?? 0) !== 1) throw new SiwxError({
261
+ code: "Unsupported",
262
+ reason: "off-chain message version"
263
+ });
264
+ if ((signed[17] ?? 0) !== 1) throw invalidEnvelope();
265
+ const signers = signed.subarray(18, 50);
266
+ if (signers.length !== 32) throw invalidEnvelope();
267
+ const content = signed.subarray(50);
268
+ if (content.length === 0) throw invalidEnvelope();
269
+ return {
270
+ signer: signers.subarray(0, 32),
271
+ content
272
+ };
273
+ }
274
+ /**
275
+ * Wrap `text` in the off-chain message v1 envelope for `signer` (validated as an SIWS address).
276
+ *
277
+ * @throws {SiwxError} `InvalidField` `address`.
278
+ */
279
+ function offchainMessage(text, signer) {
280
+ const signerBytes = addressBytes(signer);
281
+ canonicalPoint(signerBytes);
282
+ const textBytes = utf8Encode(text);
283
+ const out = new Uint8Array(50 + textBytes.length);
284
+ out.set(OFFCHAIN_DOMAIN, 0);
285
+ out[16] = 1;
286
+ out[17] = 1;
287
+ out.set(signerBytes, 18);
288
+ out.set(textBytes, 50);
289
+ return out;
290
+ }
291
+ function svmAccount(message) {
292
+ validateAddress(message.address);
293
+ return createAccountId(NAMESPACE, chainReference(message.chainId), message.address);
294
+ }
295
+ /** Create the Solana namespace profile. */
296
+ function svm() {
297
+ return {
298
+ namespace: NAMESPACE,
299
+ label: CHAIN_NAME,
300
+ account: svmAccount,
301
+ format(message) {
302
+ svmAccount(message);
303
+ return format(message);
304
+ },
305
+ parse(signed) {
306
+ if (signed.length > 16384) throw new SiwxError({
307
+ code: "MessageTooLarge",
308
+ length: signed.length,
309
+ max: MAX_MESSAGE_BYTES
310
+ });
311
+ if (signed.length >= OFFCHAIN_DOMAIN.length && equalBytes(signed.subarray(0, OFFCHAIN_DOMAIN.length), OFFCHAIN_DOMAIN)) {
312
+ const env = decodeEnvelope(signed);
313
+ const message = parseBytes(env.content);
314
+ const expected = addressBytes(message.address);
315
+ if (!equalBytes(env.signer, expected)) throw new SiwxError({
316
+ code: "Mismatch",
317
+ field: "address",
318
+ expected: message.address,
319
+ actual: b58(env.signer)
320
+ });
321
+ return message;
322
+ }
323
+ return parseBytes(signed);
324
+ },
325
+ verify: async (message, signed, signature) => settle(() => verifyEddsa(message, signed, signature))
326
+ };
327
+ }
328
+ //#endregion
329
+ export { offchainMessage, siws_exports as siws, svm, validateAddress };
package/package.json CHANGED
@@ -1,6 +1,86 @@
1
1
  {
2
2
  "name": "@qntx/siwx",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.7.0",
4
+ "description": "Sign-In with X (CAIP-122) for TypeScript: SIWE, SIWS, message parsing, validation, and signature verification.",
5
+ "keywords": [
6
+ "authentication",
7
+ "caip-122",
8
+ "ethereum",
9
+ "sign-in",
10
+ "siwe",
11
+ "siws",
12
+ "siwx",
13
+ "solana"
14
+ ],
15
+ "homepage": "https://github.com/qntx/siwx#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/qntx/siwx/issues"
18
+ },
19
+ "license": "MIT OR Apache-2.0",
20
+ "author": "QuantX <x@qntx.org>",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/qntx/siwx.git",
24
+ "directory": "packages/siwx"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "LICENSE-MIT",
29
+ "LICENSE-APACHE"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.mts",
36
+ "import": "./dist/index.mjs",
37
+ "default": "./dist/index.mjs"
38
+ },
39
+ "./evm": {
40
+ "types": "./dist/evm.d.mts",
41
+ "import": "./dist/evm.mjs",
42
+ "default": "./dist/evm.mjs"
43
+ },
44
+ "./svm": {
45
+ "types": "./dist/svm.d.mts",
46
+ "import": "./dist/svm.mjs",
47
+ "default": "./dist/svm.mjs"
48
+ },
49
+ "./package.json": "./package.json"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "build": "vp pack",
56
+ "dev": "vp pack --watch",
57
+ "test": "vp test",
58
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.src.json",
59
+ "check:pkg": "vp pack && publint && attw --pack . --profile esm-only",
60
+ "prepublishOnly": "vp run build",
61
+ "smoke:hermes": "bun scripts/hermes-smoke.ts"
62
+ },
63
+ "dependencies": {
64
+ "@noble/curves": "^2.4.0",
65
+ "@noble/hashes": "^2.4.0",
66
+ "@scure/base": "^2.4.0"
67
+ },
68
+ "devDependencies": {
69
+ "@arethetypeswrong/cli": "0.18.5",
70
+ "@qntx/tsconfig": "1.0.1",
71
+ "@types/node": "26.6.5",
72
+ "publint": "0.3.25",
73
+ "typescript": "7.0.2",
74
+ "vite-plus": "1.3.0"
75
+ },
76
+ "devEngines": {
77
+ "packageManager": {
78
+ "name": "bun",
79
+ "version": "1.4.0",
80
+ "onFail": "ignore"
81
+ }
82
+ },
83
+ "engines": {
84
+ "node": ">=20.19.0"
85
+ }
86
+ }