-module(ywt@claim). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/ywt/claim.gleam"). -export([custom/4, issuer/2, audience/2, id/2, subject/2, numeric_date_decoder/0, encode_numeric_date/1, issued_at/0, not_before/2, expires_at/2, verify/2, encode/2]). -export_type([claim/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. -opaque claim() :: {claim, binary(), boolean(), fun((gleam@dynamic:dynamic_()) -> {ok, nil} | {error, ywt@internal@core:parse_error()}), fun(() -> gleam@json:json())}. -file("src/ywt/claim.gleam", 273). -spec validate( binary(), gleam@dynamic@decode:decoder(EPT), fun((EPT) -> ywt@internal@core:parse_error()), fun((EPT) -> boolean()) ) -> fun((gleam@dynamic:dynamic_()) -> {ok, nil} | {error, ywt@internal@core:parse_error()}). validate(Name, Decoder, On_error, Validate) -> fun(Data) -> case gleam@dynamic@decode:run(Data, Decoder) of {ok, Value} -> case Validate(Value) of true -> {ok, nil}; false -> {error, On_error(Value)} end; {error, Error} -> {error, {claim_decoding_error, Name, Error}} end end. -file("src/ywt/claim.gleam", 259). ?DOC( " Creates a custom claim with a specific name and value that must match exactly during validation.\n" "\n" " Use this for application-specific claims like roles, permissions, or custom\n" " business logic requirements.\n" "\n" " ## Example\n" " ```gleam\n" " // Add role-based authorization\n" " let claims = [\n" " claim.custom(\n" " name: \"role\",\n" " value: \"admin\",\n" " encode: json.string,\n" " decoder: decode.string\n" " ),\n" " claim.custom(\n" " name: \"permissions\",\n" " value: [\"read\", \"write\", \"delete\"],\n" " encode: json.array(_, json.string),\n" " decoder: decode.list(decode.string)\n" " ),\n" " claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5))\n" " ]\n" " ```\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec custom( binary(), EPR, fun((EPR) -> gleam@json:json()), gleam@dynamic@decode:decoder(EPR) ) -> claim(). custom(Name, Value, Encode, Decoder) -> On_error = fun(_) -> {invalid_custom_claim, Name} end, Check = fun(Decoded) -> Decoded =:= Value end, Value@1 = fun() -> Encode(Value) end, Verify = validate(Name, Decoder, On_error, Check), {claim, Name, true, Verify, Value@1}. -file("src/ywt/claim.gleam", 291). -spec string_claim( binary(), binary(), list(binary()), fun((list(binary()), binary()) -> ywt@internal@core:parse_error()) ) -> claim(). string_claim(Name, Primary, Others, Error) -> Expected = [Primary | Others], Value = fun() -> gleam@json:string(Primary) end, Verify = validate( Name, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(_capture) -> Error(Expected, _capture) end, fun(_capture@1) -> gleam@list:contains(Expected, _capture@1) end ), {claim, Name, true, Verify, Value}. -file("src/ywt/claim.gleam", 69). ?DOC( " Validates that the JWT comes from a trusted issuer, and includes that issuer\n" " in generated tokens.\n" "\n" " Use this to ensure JWTs are only accepted from authorized authentication services,\n" " preventing token misuse across different services. It is common practice to\n" " set the issuer to a URL with a `.well-known/jwks.json` endpoint, from which\n" " public verification keys can be fetched.\n" "\n" " ## Example\n" " ```gleam\n" " // Only accept tokens from your auth service\n" " let claims = [\n" " claim.issuer(\"https://auth.example.com\", [])\n" " ]\n" " ```\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec issuer(binary(), list(binary())) -> claim(). issuer(Issuer, Others) -> string_claim( <<"iss"/utf8>>, Issuer, Others, fun(Field@0, Field@1) -> {invalid_issuer, Field@0, Field@1} end ). -file("src/ywt/claim.gleam", 92). ?DOC( " Validates that the JWT is intended for your service, and includes the primary\n" " audience in generated tokens.\n" "\n" " Include this to ensure JWTs are only used by their intended recipients,\n" " preventing tokens meant for other services from being accepted.\n" "\n" " ## Example\n" " ```gleam\n" " // For an API service\n" " let claims = [\n" " claim.audience(\"api.myapp.com\", [\"admin-api.myapp.com\"]),\n" " ]\n" " ```\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec audience(binary(), list(binary())) -> claim(). audience(Primary, Others) -> string_claim( <<"aud"/utf8>>, Primary, Others, fun(Field@0, Field@1) -> {invalid_audience, Field@0, Field@1} end ). -file("src/ywt/claim.gleam", 197). ?DOC( " Adds a unique identifier (jti) to the JWT for tracking and revocation.\n" "\n" " Use this when you need to track specific tokens or implement token revocation\n" " mechanisms.\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec id(binary(), list(binary())) -> claim(). id(Id, Others) -> string_claim( <<"jti"/utf8>>, Id, Others, fun(Field@0, Field@1) -> {invalid_id, Field@0, Field@1} end ). -file("src/ywt/claim.gleam", 225). ?DOC( " Identifies the subject (typically a user) that the JWT represents.\n" "\n" " Use this to bind JWTs to specific users or entities, enabling proper\n" " authorization decisions. This could be an internal user id.\n" "\n" " ## Security Considerations\n" " - Use stable, unique identifiers (user IDs, not usernames)\n" " - Don't include personally identifiable information\n" "\n" " ## Example\n" " ```gleam\n" " // Bind token to a specific user\n" " let claims = [\n" " claim.subject(\"user_12345\", []),\n" " claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),\n" " claim.audience(\"api.myapp.com\", [])\n" " ]\n" " ```\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec subject(binary(), list(binary())) -> claim(). subject(Subject, Others) -> string_claim( <<"sub"/utf8>>, Subject, Others, fun(Field@0, Field@1) -> {invalid_subject, Field@0, Field@1} end ). -file("src/ywt/claim.gleam", 320). ?DOC( " Decodes JWT numeric date values (seconds since Unix epoch) into timestamp objects.\n" "\n" " Use this when manually processing JWT claims that contain timestamp values.\n" "\n" " ## Example\n" " ```gleam\n" " // Manually decode an expiration claim\n" " let exp_decoder = decode.field(\"exp\", numeric_date_decoder())\n" " ```\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec numeric_date_decoder() -> gleam@dynamic@decode:decoder(gleam@time@timestamp:timestamp()). numeric_date_decoder() -> gleam@dynamic@decode:map( {decoder, fun gleam@dynamic@decode:decode_int/1}, fun gleam@time@timestamp:from_unix_seconds/1 ). -file("src/ywt/claim.gleam", 341). ?DOC( " Encodes a timestamp into the JWT numeric date format (seconds since Unix epoch).\n" "\n" " Use this when manually creating JWT payloads with timestamp values.\n" "\n" " ## Example\n" " ```gleam\n" " // Add custom timestamp to payload\n" " let payload = [\n" " #(\"custom_date\", encode_numeric_date(my_timestamp))\n" " ]\n" " ```\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec encode_numeric_date(gleam@time@timestamp:timestamp()) -> gleam@json:json(). encode_numeric_date(Timestamp) -> gleam@json:int( erlang:trunc(gleam@time@timestamp:to_unix_seconds(Timestamp)) ). -file("src/ywt/claim.gleam", 41). ?DOC( " Adds the current timestamp as the \"issued at\" (iat) claim.\n" "\n" " Include this in JWTs to track when tokens were issued, useful for debugging\n" " and audit trails. To reject tokens that were created in the future, use `not_before`.\n" "\n" " ## Example\n" " ```gleam\n" " let claims = [\n" " claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),\n" " ...\n" " ]\n" " ```\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec issued_at() -> claim(). issued_at() -> Value = fun() -> encode_numeric_date(gleam@time@timestamp:system_time()) end, Verify = fun(_) -> {ok, nil} end, {claim, <<"iat"/utf8>>, false, Verify, Value}. -file("src/ywt/claim.gleam", 115). ?DOC( " Adds a \"not before\" (nbf) claim that prevents the JWT from being valid until the current time.\n" "\n" " Use this for JWTs that should only become valid at a specific time, such as\n" " scheduled operations or delayed activations.\n" "\n" " ## Example\n" " ```gleam\n" " // Token becomes valid immediately, allowing 1 minute clock skew\n" " let claims = [\n" " claim.not_before(timestamp.system_time(), leeway: duration.minutes(1)),\n" " claim.expires_at(max_age: duration.hours(2), leeway: duration.minutes(1))\n" " ]\n" " ```\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec not_before( gleam@time@timestamp:timestamp(), gleam@time@duration:duration() ) -> claim(). not_before(Time, Leeway) -> Name = <<"nbf"/utf8>>, Value = fun() -> encode_numeric_date(Time) end, Verify = validate( Name, numeric_date_decoder(), fun(Field@0) -> {token_not_yet_valid, Field@0} end, fun(Not_before) -> Now = gleam@time@timestamp:system_time(), case gleam@time@timestamp:compare( Not_before, gleam@time@timestamp:add(Now, Leeway) ) of gt -> false; _ -> true end end ), {claim, Name, true, Verify, Value}. -file("src/ywt/claim.gleam", 166). ?DOC( " Sets an expiration time for the JWT, automatically rejecting expired tokens.\n" "\n" " **ALWAYS include this** - it's your primary defense against token replay attacks\n" " and limits the damage from compromised tokens.\n" "\n" " ## Security Considerations\n" " - Make expiration times as short as reasonably possible\n" " - Include reasonable leeway for clock differences\n" "\n" " ## Example\n" " ```gleam\n" " // Short-lived API access token\n" " let api_claims = [\n" " claim.expires_at(max_age: duration.minutes(30), leeway: duration.minutes(5))\n" " ]\n" "\n" " // Longer-lived refresh token\n" " let session_claims = [\n" " claim.expires_at(max_age: duration.hours(24 * 7), leeway: duration.minutes(5))\n" " ]\n" " ```\n" "\n" " 🚨 **Critical:** Never create or parse JWTs without expiration times -\n" " they would be valid forever if compromised.\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec expires_at(gleam@time@duration:duration(), gleam@time@duration:duration()) -> claim(). expires_at(Max_age, Leeway) -> Name = <<"exp"/utf8>>, Value = fun() -> _pipe = gleam@time@timestamp:system_time(), _pipe@1 = gleam@time@timestamp:add(_pipe, Max_age), encode_numeric_date(_pipe@1) end, Verify = validate( Name, numeric_date_decoder(), fun(Field@0) -> {token_expired, Field@0} end, fun(Expires_at) -> Now = gleam@time@timestamp:system_time(), case gleam@time@timestamp:compare( gleam@time@timestamp:add(Expires_at, Leeway), Now ) of gt -> true; _ -> false end end ), {claim, Name, true, Verify, Value}. -file("src/ywt/claim.gleam", 398). ?DOC(" Verify a single claim, handling required vs optional claims.\n"). -spec verify_single_claim(gleam@dynamic:dynamic_(), claim()) -> {ok, nil} | {error, ywt@internal@core:parse_error()}. verify_single_claim(Payload, Claim) -> Decoder = gleam@dynamic@decode:at( [erlang:element(2, Claim)], {decoder, fun gleam@dynamic@decode:decode_dynamic/1} ), case gleam@dynamic@decode:run(Payload, Decoder) of {ok, Claim_value} -> (erlang:element(4, Claim))(Claim_value); {error, _} when not erlang:element(3, Claim) -> {ok, nil}; {error, _} -> {error, {missing_claim, erlang:element(2, Claim)}} end. -file("src/ywt/claim.gleam", 393). ?DOC( " Validates all claims against a JWT payload.\n" "\n" " This function checks each claim in the provided list against the JWT payload,\n" " returning the first validation error encountered or success if all claims pass.\n" "\n" " 🚨 This is a low-level function used internally by ywt.\n" "\n" " ## Example\n" " ```gleam\n" " let payload = // ... decoded JWT payload\n" "\n" " let security_claims = [\n" " expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),\n" " issuer(\"https://auth.myapp.com\", []),\n" " audience(\"https://api.myapp.com\", []),\n" " custom(\"role\", \"admin\", json.string, decode.string)\n" " ]\n" "\n" " case verify(payload, security_claims) {\n" " Ok(Nil) -> {\n" " // All claims validated successfully\n" " process_authenticated_request(payload)\n" " }\n" " Error(TokenExpired(expired_at)) -> {\n" " log.info(\"Token expired at: \" <> timestamp.to_string(expired_at))\n" " return_authentication_error()\n" " }\n" " Error(InvalidIssuer(expected, actual)) -> {\n" " log.warning(\"Invalid issuer - expected: \" <> expected <> \", got: \" <> actual)\n" " return_forbidden_error()\n" " }\n" " Error(error) -> {\n" " log.error(\"Claim validation failed: \" <> string.inspect(error))\n" " return_bad_request_error()\n" " }\n" " }\n" " ```\n" "\n" " ⚠️ **Important:** This function only validates claims - it does not verify\n" " cryptographic signatures. Use the full `decode()` function for complete JWT validation.\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec verify(gleam@dynamic:dynamic_(), list(claim())) -> {ok, nil} | {error, ywt@internal@core:parse_error()}. verify(Payload, Claims) -> gleam@list:try_each( Claims, fun(_capture) -> verify_single_claim(Payload, _capture) end ). -file("src/ywt/claim.gleam", 446). ?DOC( " Encode claims and application data into JSON.\n" "\n" " This function merges application data with JWT claims into a single JSON object\n" " for token encoding. When the same field appears in both lists, claims take\n" " precedence to ensure security-critical values cannot be overridden.\n" "\n" " 🚨 This is a low-level function used internally by ywt.\n" "\n" " ## Example\n" " ```gleam\n" " let user_data = [\n" " #(\"name\", json.string(\"Alice\")),\n" " #(\"role\", json.string(\"user\")),\n" " #(\"exp\", json.int(1234567890)) // This will be overridden\n" " ]\n" "\n" " let security_claims = [\n" " expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),\n" " custom(\"role\", \"admin\", json.string, decode.string) // This takes precedence\n" " ]\n" "\n" " let payload = encode(security_claims, user_data)\n" " // Result: {\"name\": \"Alice\", \"role\": \"admin\", \"exp\": }\n" " ```\n" "\n" " 💡 **Best Practice:** Place security-sensitive fields in claims rather than\n" " data to validate them and ensure they cannot be accidentally overridden.\n" "\n" "
\n" " \n" " Back to top ↑\n" " \n" "
\n" ). -spec encode(list(claim()), list({binary(), gleam@json:json()})) -> gleam@json:json(). encode(Claims, Data) -> _pipe = maps:new(), _pipe@1 = gleam@list:fold( Data, _pipe, fun(Dict, Pair) -> gleam@dict:insert( Dict, erlang:element(1, Pair), erlang:element(2, Pair) ) end ), _pipe@2 = gleam@list:fold( Claims, _pipe@1, fun(Dict@1, Claim) -> gleam@dict:insert( Dict@1, erlang:element(2, Claim), (erlang:element(5, Claim))() ) end ), _pipe@3 = maps:to_list(_pipe@2), gleam@json:object(_pipe@3).