@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.
Its only dependency is the peer @tuwaio/orbit-core, which has no dependencies of its own and reads the CAIP-2 chain and CAIP-10 account IDs and the Solana genesis-hash chain IDs. The package 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) and throwsSiwxValidationErrorwhen a required field is missing or a field contains a line break;parseMessagereads the text back and throwsSiwxParseErrorwhen it is malformed, including a header whose domain is not an RFC 3986 authority. - Validation:
validateMessagechecks field formats (domainas an RFC 3986 authority such asapp.example.comorlocalhost:3000, CAIP-10 address, CAIP-2 chain ID,http(s)URI, nonce, ISO 8601 timestamps) and expiration, and reports every failure at once. Addresses and chain IDs are read with the parsers of@tuwaio/orbit-core, so an EVM or Solana account must be a valid address of its chain. - 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 @tuwaio/orbit-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 only checks that the message can be built: every required field is a non-empty string and no field contains a line break, which would add lines to the signed text (it throws SiwxValidationError otherwise). Run validateMessage on untrusted fields for the formats and timing.
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:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
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. The one exception is a Solana cluster: its Wallet Standard name and its genesis-hash ID match each other (solana:devnetandsolana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1), so sessions signed before the switch to genesis-hash IDs stay valid.normalizeSolanaChainIdandisChainIdAllowedapply this rule.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