@tuwaio/siwx-core
@tuwaio/siwx-core is the Layer 1 (L1) package of SIWX (Sign-In With X), the authentication project of TUWA Stage 1 (“Core Auth & Primitives”, next to Orbit Utils). It implements the CAIP-122 message format: it builds, parses and validates chain-agnostic sign-in messages for EVM (eip155) and Solana accounts, and defines the types and errors that the SIWX L2 packages share.
The package has zero runtime dependencies and imports no Web3 SDK. It runs in browsers, Node.js 20+ and edge runtimes; only generateNonce needs the Web Crypto API.
🏛️ Core Capabilities
- Message format:
buildMessageformats CAIP-122 fields into the text the wallet signs (the EIP-4361 layout used by CAIP-122);parseMessagereads the text back and throwsSiwxParseErrorwhen it is malformed. - Validation:
validateMessagechecks field formats (CAIP-10 address, CAIP-2 chain ID,http(s)URI, nonce, ISO 8601 timestamps) and expiration, and reports every failure at once. - Verification policy:
validatePolicyandSiwxVerificationPolicybind a message to your domain, URI, allowed chains and time windows (issuedAtage,notBefore, maximum lifetime, clock skew). - Session matching:
isSessionMatchingTargetchecks that a session belongs to a given address and chain, case-insensitively for EVM and case-sensitively for Solana. - Nonces and errors:
generateNoncereturns 32 random hex characters;SiwxErrorand its subclasses carry machine-readablecodes.
Signatures are verified by the chain packages (@tuwaio/siwx-evm, @tuwaio/siwx-solana) and on the server by @tuwaio/siwx-server.
💾 Installation
pnpm add @tuwaio/siwx-core🚀 Usage
Building and parsing a message
import { buildMessage, generateNonce, parseMessage } from '@tuwaio/siwx-core';
const message = buildMessage({
domain: 'app.tuwa.io',
address: 'eip155:1:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
statement: 'Sign in to TUWA.',
uri: 'https://app.tuwa.io',
version: '1',
chainId: 'eip155:1',
nonce: generateNonce(),
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
});
// Throws SiwxParseError if the text is not a CAIP-122 message.
const fields = parseMessage(message);The message the wallet shows and signs:
app.tuwa.io wants you to sign in with your blockchain account:
eip155:1:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B
Sign in to TUWA.
URI: https://app.tuwa.io
Version: 1
Chain ID: eip155:1
Nonce: 3f9c1e2a7b4d8f6051c2e9a0d7b3f418
Issued At: 2026-09-26T10:00:00.000Z
Expiration Time: 2026-09-26T10:10:00.000ZbuildMessage does not validate its input; run validateMessage on untrusted fields.
Validating fields and a policy
import { parseMessage, validateMessage } from '@tuwaio/siwx-core';
declare const message: string;
const result = validateMessage(parseMessage(message), {
policy: {
expectedDomain: 'app.tuwa.io',
expectedUri: 'https://app.tuwa.io',
allowedChainIds: ['eip155:1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpK'],
requireExpirationTime: true,
maxIssuedAtAgeSeconds: 300,
maxSessionLifetimeSeconds: 24 * 60 * 60,
},
});
if (!result.valid) {
console.error(result.errors); // one human-readable string per failed check
}validateMessagechecks formats and timing only. It does not verify the signature.- Policy rules run only for the fields you set;
maxIssuedAtAgeSecondshas no default.clockSkewSecondsdefaults to 60 seconds. - Two timing rules always apply, even without a policy:
issuedAtmust not be in the future andnotBeforemust have been reached (turn the latter off withenforceNotBefore: false). allowedChainIdsentries are full CAIP-2 IDs matched exactly:eip155:1does not allowsolana:1, and a bare1matches nothing.validatePolicy(fields, policy, now?)runs the policy rules alone and returns the list of violations.
Matching a session to a wallet
import { isSessionMatchingTarget } from '@tuwaio/siwx-core';
const session = { address: 'eip155:1:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', chainId: 'eip155:1' };
isSessionMatchingTarget(session, '0xab5801a7d398351b8be11c439e05c5b3259aec9b'); // true: EVM addresses ignore case
isSessionMatchingTarget(session, 'eip155:1:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', 'eip155:1'); // true
isSessionMatchingTarget(session, '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', 10); // false: other chainHandling errors
import { parseMessage, SiwxParseError } from '@tuwaio/siwx-core';
try {
parseMessage('not a CAIP-122 message');
} catch (error) {
if (error instanceof SiwxParseError) {
console.error(error.code, error.message); // "SIWX_PARSE_ERROR", "Message is too short to be a valid CAIP-122 message."
}
}parseMessage and generateNonce are the only functions of this package that throw. The verifiers of the L2 packages return { success: false, error } instead of throwing. The policy error classes (SiwxPolicyViolationError and its subclasses) are never thrown by SIWX; they are available for your own checks.
📚 API Reference
Every export, with signatures and types generated from the source, is documented at siwx.docs.tuwa.io/packages/siwx-core .
📄 License
Licensed under the Apache-2.0 License. See the LICENSE file for details.
Classes
- SiwxChainNotAllowedError
- SiwxDomainMismatchError
- SiwxError
- SiwxExpiredSessionError
- SiwxIssuedAtFutureError
- SiwxIssuedAtStaleError
- SiwxNonceReplayError
- SiwxNotBeforeError
- SiwxParseError
- SiwxPolicyViolationError
- SiwxSessionLifetimeExceededError
- SiwxUnsupportedNamespaceError
- SiwxUriMismatchError
- SiwxValidationError
- SiwxVerificationError
Interfaces
- SiwxAdapter
- SiwxMessageFields
- SiwxSessionLike
- SiwxValidationResult
- SiwxVerificationPolicy
- SiwxVerifyPayload
- SiwxVerifyResult
- ValidateMessageOptions