-module(ywt). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/ywt.gleam"). -export([generate_key/1, sign_bits/2, sign_string/2, verify_bits/3, verify_string/3, decode_unsafely_without_validation/2, decode/4, encode/3]). -export_type([error/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " \n" " \n" ). -type error() :: malformed_token | invalid_header_encoding | invalid_payload_encoding | invalid_signature_encoding | {invalid_header_json, gleam@json:decode_error()} | {header_decoding_error, list(gleam@dynamic@decode:decode_error())} | unsupported_critical_header | unsupported_unencoded_payload | {invalid_payload_json, gleam@json:decode_error()} | no_matching_key | invalid_signature | {token_expired, gleam@time@timestamp:timestamp()} | {token_not_yet_valid, gleam@time@timestamp:timestamp()} | {invalid_issuer, list(binary()), binary()} | {invalid_audience, list(binary()), list(binary())} | {invalid_type, binary(), binary()} | {invalid_subject, list(binary()), binary()} | {invalid_id, list(binary()), binary()} | {missing_claim, binary()} | {claim_decoding_error, binary(), list(gleam@dynamic@decode:decode_error())} | {invalid_custom_claim, binary()} | {payload_decoding_error, list(gleam@dynamic@decode:decode_error())}. -file("src/ywt.gleam", 96). -spec from_core_error(ywt@internal@core:error()) -> error(). from_core_error(Error) -> case Error of {claim_decoding_error, Claim_name, Error@1} -> {claim_decoding_error, Claim_name, Error@1}; {invalid_audience, Expected, Actual} -> {invalid_audience, Expected, Actual}; {invalid_custom_claim, Claim_name@1} -> {invalid_custom_claim, Claim_name@1}; {header_decoding_error, Error@2} -> {header_decoding_error, Error@2}; invalid_header_encoding -> invalid_header_encoding; {invalid_header_json, Error@3} -> {invalid_header_json, Error@3}; {invalid_id, Expected@1, Actual@1} -> {invalid_id, Expected@1, Actual@1}; {invalid_issuer, Expected@2, Actual@2} -> {invalid_issuer, Expected@2, Actual@2}; {invalid_type, Expected@3, Actual@3} -> {invalid_type, Expected@3, Actual@3}; invalid_payload_encoding -> invalid_payload_encoding; {invalid_payload_json, Error@4} -> {invalid_payload_json, Error@4}; invalid_signature -> invalid_signature; invalid_signature_encoding -> invalid_signature_encoding; unsupported_critical_header -> unsupported_critical_header; unsupported_unencoded_payload -> unsupported_unencoded_payload; {invalid_subject, Expected@4, Actual@4} -> {invalid_subject, Expected@4, Actual@4}; malformed_token -> malformed_token; {missing_claim, Claim_name@2} -> {missing_claim, Claim_name@2}; no_matching_key -> no_matching_key; {payload_decoding_error, Error@5} -> {payload_decoding_error, Error@5}; {token_expired, Expired_at} -> {token_expired, Expired_at}; {token_not_yet_valid, Not_before} -> {token_not_yet_valid, Not_before} end. -file("src/ywt.gleam", 135). ?DOC( " Generates a signing key for an algorithm.\n" "\n" " Generated keys include a random `kid` for key rotation. Store signing keys as\n" " secrets; anyone with one can mint trusted tokens.\n" "\n" " ```gleam\n" " let signing_key = ywt.generate_key(algorithm.es384)\n" " let verify_key = verify_key.derived(signing_key)\n" " ```\n" ). -spec generate_key(ywt@algorithm:algorithm()) -> ywt@sign_key:sign_key(). generate_key(Algorithm) -> Key = ywt@algorithm:generate_key( Algorithm, fun ywt_ffi:generate_hmac/1, fun ywt_ffi:generate_ecdsa/2, fun ywt_ffi:generate_rsa/3 ), ywt@sign_key:with_random_id(Key). -file("src/ywt.gleam", 168). ?DOC( " Signs bytes with a signing key.\n" "\n" " This is a low-level function used internally by ywt.\n" "\n" " This is a low-level primitive. Most applications should use `encode` so the\n" " JWT header, payload, and claims are constructed consistently. Raw signatures\n" " do not carry claim or audience validation.\n" ). -spec sign_bits(bitstring(), ywt@sign_key:sign_key()) -> bitstring(). sign_bits(Message, Key) -> ywt_ffi:sign(Message, Key). -file("src/ywt.gleam", 178). ?DOC( " Signs a UTF-8 string and returns an unpadded base64url signature.\n" "\n" " This is a low-level helper for JWT-style signing input. Prefer `encode` for\n" " complete JWTs.\n" "\n" " ```gleam\n" " ywt.sign_string(\"header.payload\", signing_key)\n" " ```\n" ). -spec sign_string(binary(), ywt@sign_key:sign_key()) -> binary(). sign_string(Message, Key) -> Bits = ywt_ffi:sign(<>, Key), gleam@bit_array:base64_url_encode(Bits, false). -file("src/ywt.gleam", 193). ?DOC( " Verifies a byte signature with a verification key.\n" "\n" " This is a low-level function used internally by ywt.\n" "\n" " This only verifies the signature over the exact bytes you pass. It does not\n" " parse JWTs or validate claims.\n" "\n" " RSA JWKs decoded without `alg` cannot be used for raw verification and return\n" " `False`; use `decode` when the signed JWT header should choose the algorithm.\n" ). -spec verify_bits(bitstring(), bitstring(), ywt@verify_key:verify_key()) -> boolean(). verify_bits(Message, Signature, Key) -> ywt_ffi:verify(Message, Signature, Key). -file("src/ywt.gleam", 203). ?DOC( " Verifies an unpadded base64url signature for a UTF-8 string.\n" "\n" " Invalid base64url returns `False`. This only verifies the signature over the\n" " exact string you pass; use `decode` for complete JWT verification.\n" ). -spec verify_string(binary(), binary(), ywt@verify_key:verify_key()) -> boolean(). verify_string(Message, Signature, Key) -> case ywt@internal@base64url:decode(Signature) of {ok, Signature@1} -> ywt_ffi:verify(<>, Signature@1, Key); {error, _} -> false end. -file("src/ywt.gleam", 219). ?DOC( " Decodes a JWT payload without verifying the signature or claims.\n" "\n" " Do not use this for authentication or authorization. The token may be forged,\n" " expired, or intended for another audience. No `exp`, `nbf`, or `aud` checks\n" " are performed.\n" ). -spec decode_unsafely_without_validation( binary(), gleam@dynamic@decode:decoder(FTV) ) -> {ok, FTV} | {error, error()}. decode_unsafely_without_validation(Jwt, Payload_decoder) -> _pipe = ywt@internal@jwt:decode_unsafely_without_validation( Jwt, Payload_decoder ), gleam@result:map_error(_pipe, fun from_core_error/1). -file("src/ywt.gleam", 247). ?DOC( " Verifies a JWT and decodes its payload.\n" "\n" " Tokens with `exp` and `nbf` are checked by default with zero leeway. Tokens\n" " with `aud` are rejected by default unless you pass an `audience` claim.\n" " Audience validation accepts string or array values.\n" "\n" " Unknown non-critical JWT header fields are ignored by ywt. Tokens with a\n" " `crit` header parameter or `b64: false` are rejected. Unknown payload fields\n" " are accepted or rejected by your payload decoder.\n" "\n" " ```gleam\n" " let claims = [\n" " claim.expires_at(max_age: duration.minutes(15), leeway: duration.minutes(1)),\n" " claim.issuer(\"https://auth.example.com\", []),\n" " claim.audience(\"https://api.example.com\", []),\n" " ]\n" "\n" " let decoder = decode.field(\"sub\", decode.string)\n" " let result = ywt.decode(jwt, using: decoder, claims:, keys: [verify_key])\n" " ```\n" ). -spec decode( binary(), gleam@dynamic@decode:decoder(FTZ), list(ywt@claim:claim()), list(ywt@verify_key:verify_key()) ) -> {ok, FTZ} | {error, error()}. decode(Jwt, Decoder, Claims, Keys) -> Verify = fun(Message, Signature, Key, Next) -> Next(ywt_ffi:verify(Message, Signature, Key)) end, Resolve = fun(_capture) -> gleam@result:map_error(_capture, fun from_core_error/1) end, ywt@internal@jwt:decode(Jwt, Decoder, Claims, Keys, Verify, Resolve). -file("src/ywt.gleam", 280). ?DOC( " Creates a signed JWT from payload data and claims.\n" "\n" " JWTs are signed, not encrypted, so anyone with the token can read the\n" " payload. Claims take precedence when a field appears in both `payload` and\n" " `claims`; put security fields such as `exp`, `iss`, and `aud` in claims.\n" "\n" " ```gleam\n" " let payload = [\n" " #(\"sub\", json.string(\"user_123\")),\n" " #(\"role\", json.string(\"admin\")),\n" " ]\n" "\n" " let claims = [\n" " claim.expires_at(max_age: duration.minutes(15), leeway: duration.minutes(1)),\n" " claim.issuer(\"https://auth.example.com\", []),\n" " claim.audience(\"https://api.example.com\", []),\n" " ]\n" "\n" " let jwt = ywt.encode(payload, claims, signing_key)\n" " ```\n" ). -spec encode( list({binary(), gleam@json:json()}), list(ywt@claim:claim()), ywt@sign_key:sign_key() ) -> binary(). encode(Payload, Claims, Key) -> Sign = fun(Message, Key@1, Next) -> Next(ywt_ffi:sign(Message, Key@1)) end, ywt@internal@jwt:encode(Payload, Claims, Key, Sign).