@gramota/sd-jwt
SD-JWT-VC parser, hash binding, KB-JWT issuance / verification.
Install: pnpm add @gramota/sd-jwt
Source: github.com/gramota-org/gramota/tree/main/packages/sd-jwt
Classes
SdJwtError
Defined in: @gramota/sd-jwt/dist/types.d.ts:25
Single error class for every failure mode in @gramota/sd-jwt.
Discriminator is the code field — handlers branch on the prefix
(sd_jwt.parse.* / sd_jwt.kb.* / ...) rather than instanceof on
a per-operation class. Replaces the older per-operation classes
(SdJwtParseError, SdJwtVerificationError, SdJwtIssuanceError,
SdJwtKeyBindingError).
Extends
GramotaError
Constructors
Constructor
new SdJwtError(
code: SdJwtErrorCode,
message: string,
options?: {
cause?: unknown;
}): SdJwtError;
Defined in: @gramota/sd-jwt/dist/types.d.ts:27
Parameters
code
message
string
options?
cause?
unknown
Returns
Overrides
GramotaError.constructor
Properties
cause?
readonly optional cause?: unknown;
Defined in: .pnpm/@gramota+core@0.2.1/node_modules/@gramota/core/dist/error.d.ts:44
Optional original error that caused this one. Always set when the
Gramota package is wrapping a thrown exception from a dependency
(Web Crypto, JOSE, fetch). Survives JSON.stringify(err) only via
the cause property — Node 16.9+ logs it natively.
Inherited from
GramotaError.cause
code
readonly code: SdJwtErrorCode;
Defined in: @gramota/sd-jwt/dist/types.d.ts:26
Stable string that identifies the failure mode. Subclasses narrow the type; at runtime it's always a string. Use for branching, logs, and metrics labels — never serialize GramotaError.message for that purpose, message strings drift across versions.
Overrides
GramotaError.code
name
name: string;
Defined in: .pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts:1076
Inherited from
GramotaError.name
message
message: string;
Defined in: .pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts:1077
Inherited from
GramotaError.message
stack?
optional stack?: string;
Defined in: .pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts:1078
Inherited from
GramotaError.stack
Interfaces
IssueSdJwtOptions
Defined in: @gramota/sd-jwt/dist/issue.d.ts:2
Properties
payload
payload: Record<string, unknown>;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:4
Non-selectively-disclosable claims placed directly in the JWT payload.
sdClaims?
optional sdClaims?: Record<string, unknown>;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:6
Claims to make selectively disclosable as object properties.
alg
alg: string;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:10
JWT signing algorithm (placed in header alg). The signature itself is
produced by signer — this library does not perform cryptographic signing
(that is @gramota/jose's job).
typ?
optional typ?: string;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:12
Optional typ header claim (e.g. "vc+sd-jwt", "dc+sd-jwt").
signer
signer: (signedPayload: string) => string | Promise<string>;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:15
Async (or sync) signer. Receives header.payload (the bytes to sign) and
returns the base64url-encoded signature. Use stubSignature for tests.
Parameters
signedPayload
string
Returns
string | Promise<string>
hashAlg?
optional hashAlg?: HashAlg;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:17
Hash algorithm (default "sha-256"). Sets _sd_alg when sdClaims present.
saltGenerator?
optional saltGenerator?: () => string;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:20
Salt generator returning a base64url string. Pluggable for deterministic testing. Default: 128-bit random salt.
Returns
string
extraHeader?
optional extraHeader?: Record<string, unknown>;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:22
Additional header parameters (kid, x5c, etc.).
IssuanceResult
Defined in: @gramota/sd-jwt/dist/issue.d.ts:25
Properties
token
token: string;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:26
disclosures
disclosures: SdJwtDisclosure[];
Defined in: @gramota/sd-jwt/dist/issue.d.ts:27
VerifyKbJwtOptions
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:35
Properties
expectedAudience
expectedAudience: string | readonly string[];
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:45
Required. The verifier's identifier; the KB-JWT's aud must equal
this (string form) or be present in this list (array form).
In OID4VP+x509_san_dns, two aud shapes appear in the wild:
- the verifier's audience URL (e.g.
https://verifier.example), which is what the SD-JWT-VC spec implies; and - the OID4VP
client_idvalue (e.g.x509_san_dns:verifier.example), which the EU reference wallet sends. Pass an array with both to accept either.
expectedNonce
expectedNonce: string;
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:47
Required. The challenge the verifier issued; the KB-JWT's nonce must equal this.
maxAgeSeconds?
optional maxAgeSeconds?: number;
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:49
Maximum acceptable age of the KB-JWT in seconds. Default 60.
maxClockSkewSeconds?
optional maxClockSkewSeconds?: number;
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:51
Maximum acceptable clock skew in seconds (iat in the future). Default 30.
algorithms?
optional algorithms?: readonly SupportedAlg[];
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:53
Algorithm allowlist for the KB-JWT signature. Default: all asymmetric.
now?
optional now?: () => number;
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:55
Override "now" for tests. Returns Unix seconds.
Returns
number
SdJwtHeader
Defined in: @gramota/sd-jwt/dist/types.d.ts:31
Indexable
[key: string]: unknown
Properties
alg
alg: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:32
typ?
optional typ?: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:33
kid?
optional kid?: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:34
x5c?
optional x5c?: string[];
Defined in: @gramota/sd-jwt/dist/types.d.ts:35
SdJwtPayload
Defined in: @gramota/sd-jwt/dist/types.d.ts:38
Indexable
[key: string]: unknown
Properties
iss?
optional iss?: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:39
sub?
optional sub?: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:40
iat?
optional iat?: number;
Defined in: @gramota/sd-jwt/dist/types.d.ts:41
exp?
optional exp?: number;
Defined in: @gramota/sd-jwt/dist/types.d.ts:42
nbf?
optional nbf?: number;
Defined in: @gramota/sd-jwt/dist/types.d.ts:43
cnf?
optional cnf?: {
jwk?: unknown;
kid?: string;
};
Defined in: @gramota/sd-jwt/dist/types.d.ts:44
jwk?
optional jwk?: unknown;
kid?
optional kid?: string;
vct?
optional vct?: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:48
status?
optional status?: unknown;
Defined in: @gramota/sd-jwt/dist/types.d.ts:49
_sd?
optional _sd?: string[];
Defined in: @gramota/sd-jwt/dist/types.d.ts:50
_sd_alg?
optional _sd_alg?: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:51
SdJwtDisclosure
Defined in: @gramota/sd-jwt/dist/types.d.ts:54
Properties
raw
raw: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:55
salt
salt: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:56
name
name: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:57
value
value: unknown;
Defined in: @gramota/sd-jwt/dist/types.d.ts:58
ParsedSdJwt
Defined in: @gramota/sd-jwt/dist/types.d.ts:60
Properties
header
header: SdJwtHeader;
Defined in: @gramota/sd-jwt/dist/types.d.ts:61
payload
payload: SdJwtPayload;
Defined in: @gramota/sd-jwt/dist/types.d.ts:62
signature
signature: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:63
signedPayload
signedPayload: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:64
disclosures
disclosures: SdJwtDisclosure[];
Defined in: @gramota/sd-jwt/dist/types.d.ts:65
keyBindingJwt?
optional keyBindingJwt?: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:66
presentationPrefix
presentationPrefix: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:70
The exact bytes the KB-JWT's sd_hash is computed over: the issuer JWS
plus every presented disclosure plus separator tildes, ending with ~.
Per IETF SD-JWT §4.3: sd_hash = base64url(SHA-256(presentationPrefix)).
VerifiedKeyBinding
Defined in: @gramota/sd-jwt/dist/types.d.ts:73
Verified Key Binding JWT contents per IETF SD-JWT §4.3.
Properties
header
header: {
typ: "kb+jwt";
alg: string;
};
Defined in: @gramota/sd-jwt/dist/types.d.ts:74
typ
typ: "kb+jwt";
alg
alg: string;
payload
payload: {
iat: number;
aud: string;
nonce: string;
sd_hash: string;
};
Defined in: @gramota/sd-jwt/dist/types.d.ts:78
iat
iat: number;
aud
aud: string;
nonce
nonce: string;
sd_hash
sd_hash: string;
holderKey
holderKey: Record<string, unknown>;
Defined in: @gramota/sd-jwt/dist/types.d.ts:85
The holder JWK extracted from the parent SD-JWT's cnf.jwk claim.
VerifiedSdJwt
Defined in: @gramota/sd-jwt/dist/types.d.ts:87
Properties
parsed
parsed: ParsedSdJwt;
Defined in: @gramota/sd-jwt/dist/types.d.ts:88
claims
claims: Record<string, unknown>;
Defined in: @gramota/sd-jwt/dist/types.d.ts:92
The JWT payload with _sd arrays expanded into their disclosed claims and
_sd_alg stripped. Withheld digests and decoys disappear silently — that
is the privacy property of selective disclosure.
matchedDisclosures
matchedDisclosures: SdJwtDisclosure[];
Defined in: @gramota/sd-jwt/dist/types.d.ts:94
Disclosures whose digest matched some _sd entry in the payload.
unmatchedDisclosures
unmatchedDisclosures: SdJwtDisclosure[];
Defined in: @gramota/sd-jwt/dist/types.d.ts:98
Disclosures presented by the holder that did NOT match any digest. A non-empty array here is a verification failure: the holder is presenting material the issuer never signed.
hashAlgorithm
hashAlgorithm: string;
Defined in: @gramota/sd-jwt/dist/types.d.ts:100
The hash algorithm used (from _sd_alg, defaults to "sha-256").
Type Aliases
HashAlg
type HashAlg = "sha-256" | "sha-384" | "sha-512";
Defined in: @gramota/sd-jwt/dist/issue.d.ts:24
BuildKbJwtOptions
type BuildKbJwtOptions = {
aud: string;
nonce: string;
iat?: number;
hashAlg?: HashAlg;
} & KbJwtSignerInput;
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:16
Type Declaration
aud
aud: string;
Verifier identifier — bound into KB-JWT to prevent cross-verifier replay.
nonce
nonce: string;
Verifier challenge — bound into KB-JWT to prevent within-verifier replay.
iat?
optional iat?: number;
Issued-at (Unix seconds). Default: now.
hashAlg?
optional hashAlg?: HashAlg;
Hash algorithm for sd_hash. Default sha-256. Must equal the parent
SD-JWT's _sd_alg to be verifiable.
SdJwtErrorCode
type SdJwtErrorCode =
| "sd_jwt.parse.invalid_input"
| "sd_jwt.parse.missing_separator"
| "sd_jwt.parse.malformed_jwt"
| "sd_jwt.parse.malformed_header"
| "sd_jwt.parse.malformed_payload"
| "sd_jwt.parse.malformed_disclosure"
| "sd_jwt.verify.unsupported_hash_alg"
| "sd_jwt.kb.invalid_input"
| "sd_jwt.kb.absent"
| "sd_jwt.kb.cnf_missing"
| "sd_jwt.kb.cnf_jwk_missing"
| "sd_jwt.kb.malformed"
| "sd_jwt.kb.malformed_header"
| "sd_jwt.kb.typ_mismatch"
| "sd_jwt.kb.signature_invalid"
| "sd_jwt.kb.required_claim_missing"
| "sd_jwt.kb.invalid_claim_type"
| "sd_jwt.kb.audience_mismatch"
| "sd_jwt.kb.nonce_mismatch"
| "sd_jwt.kb.iat_too_future"
| "sd_jwt.kb.iat_too_old"
| "sd_jwt.kb.transcript_mismatch"
| "sd_jwt.kb.sd_hash_compute_failed"
| "sd_jwt.issue.signer_required"
| "sd_jwt.issue.alg_required"
| "sd_jwt.issue.signer_returned_empty"
| "sd_jwt.issue.unsupported_hash_alg"
| "sd_jwt.issue.salt_generator_exhausted";
Defined in: @gramota/sd-jwt/dist/types.d.ts:15
Stable identifiers for every failure mode in @gramota/sd-jwt.
Codes are namespaced by the operation that raised them:
sd_jwt.parse.*—parseSdJwt(structural decoding)sd_jwt.verify.*—verifyHashBinding(disclosure ↔ digest match)sd_jwt.kb.*—verifyKeyBinding/buildKeyBindingJwt(KB-JWT)sd_jwt.issue.*—issueSdJwt(signing path)
Use the code (not the message) for stable log filtering, dashboards, and per-error programmatic handling. Messages are human-readable and may change.
Variables
stubSignature
const stubSignature: () => string;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:30
Constant placeholder for tests where the signature is not verified.
Returns
string
Functions
issueSdJwt()
function issueSdJwt(opts: IssueSdJwtOptions): Promise<IssuanceResult>;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:47
Build a compact-serialized SD-JWT-VC.
The encoder:
- Serialises each (salt, name, value) as a JSON array, base64url-encodes it, and SHA-256 hashes the encoded form to produce a digest.
- Places the digests in
_sd(only at top level for now), and_sd_algin the JWT payload. - Concatenates JWT + disclosures + trailing
~.
Limitations of this version:
- Object-property selective disclosure only (no nested SD, no array-element disclosures). Those are tracked for follow-up packages.
- No cryptographic signing — signer is pluggable but
@gramota/josewill provide the real ES256/EdDSA/RS256 implementations.
Parameters
opts
Returns
Promise<IssuanceResult>
deterministicSalts()
function deterministicSalts(salts: readonly string[]): () => string;
Defined in: @gramota/sd-jwt/dist/issue.d.ts:50
Build a deterministic salt generator from an array of pre-chosen salts. Useful for tests that need byte-stable output.
Parameters
salts
readonly string[]
Returns
() => string
buildKeyBindingJwt()
function buildKeyBindingJwt(presentationPrefix: string, options: BuildKbJwtOptions): Promise<string>;
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:34
Build and sign a Key Binding JWT for a given presentation prefix.
The presentation prefix is — every byte the
KB-JWT must commit to. Pass parsed.presentationPrefix from the parser, or
reconstruct it from the SD-JWT you're presenting.
Parameters
presentationPrefix
string
options
Returns
Promise<string>
verifyKeyBinding()
function verifyKeyBinding(parsed: ParsedSdJwt, options: VerifyKbJwtOptions): Promise<VerifiedKeyBinding>;
Defined in: @gramota/sd-jwt/dist/key-binding.d.ts:71
Verify a Key Binding JWT against IETF SD-JWT §4.3.
Hard rules enforced:
- KB-JWT must be present.
- Parent SD-JWT must contain a
cnf.jwkclaim. - KB-JWT header
typMUST bekb+jwt. - KB-JWT signature MUST verify against
cnf.jwkusing an allowlisted alg. - KB-JWT payload MUST contain
iat,aud,nonce,sd_hash. audMUST equalexpectedAudience.nonceMUST equalexpectedNonce.iatMUST be within (-maxAge, +clockSkew) of now.sd_hashMUST equal the verifier's own computation overpresentationPrefix.
Parameters
parsed
options
Returns
Promise<VerifiedKeyBinding>
parseSdJwt()
function parseSdJwt(token: string): ParsedSdJwt;
Defined in: @gramota/sd-jwt/dist/parse.d.ts:2
Parameters
token
string
Returns
computeSdHash()
function computeSdHash(presentationPrefix: string, hashAlg?: HashAlg): string;
Defined in: @gramota/sd-jwt/dist/sd-hash.d.ts:13
Compute sd_hash per IETF SD-JWT §4.3.
Input: the presentation prefix — issuer JWS + all presented disclosures +
separator tildes, ending with ~. NEVER include the KB-JWT.
Output: base64url-encoded hash of the input bytes.
This binds the KB-JWT to the exact disclosures presented; tampering with any disclosure or reordering them invalidates the hash.
Parameters
presentationPrefix
string
hashAlg?
Returns
string
verifyHashBinding()
function verifyHashBinding(parsed: ParsedSdJwt): VerifiedSdJwt;
Defined in: @gramota/sd-jwt/dist/verify-hash-binding.d.ts:13
Verify the hash binding between disclosures and the issuer's _sd digests,
and reconstruct the disclosed claims.
This is the SD-JWT security primitive: it proves that every disclosed claim was authorised by the issuer at signing time, and that no extra claims have been smuggled in by the holder.
Note: this verifies the hash binding only. Issuer-signature verification
(@gramota/jose) and key-binding-JWT verification are separate layers above.