diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json index 094f779..270ccab 100644 --- a/.github/.release-please-manifest.json +++ b/.github/.release-please-manifest.json @@ -1,4 +1,4 @@ { - "packages/flarelette-jwt-ts": "1.8.2", + "packages/flarelette-jwt-ts": "1.8.3", "packages/flarelette-jwt-py": "1.8.3" } diff --git a/.github/release-please-config.json b/.github/release-please-config.json index b25a9d9..8919965 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -7,6 +7,14 @@ "component": "flarelette-jwt-ts", "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, + "extra-files": [ + { + "type": "json", + "path": "packages/flarelette-jwt-py/pyproject.toml", + "jsonpath": "$.project.version" + }, + "packages/flarelette-jwt-py/flarelette_jwt/__init__.py" + ], "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "fix", "section": "Bug Fixes" }, @@ -24,6 +32,13 @@ "component": "flarelette-jwt-py", "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, + "extra-files": [ + { + "type": "json", + "path": "packages/flarelette-jwt-ts/package.json", + "jsonpath": "$.version" + } + ], "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "fix", "section": "Bug Fixes" }, diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 79607c0..fb6e7a1 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -17,7 +17,7 @@ The TypeScript package depends on the following NPM packages: @flarelette/jwt-kit-env@1.8.1 │ C:\Users\chris\git\flarelette-jwt-kit │ -└─┬ @chrislyons-dev/flarelette-jwt@1.8.1 -> .\packages\flarelette-jwt-ts +└─┬ @chrislyons-dev/flarelette-jwt@1.8.3 -> .\packages\flarelette-jwt-ts │ Environment-driven JWT authentication for Cloudflare Workers with secret-name indirection └── jose@5.10.0 JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes @@ -77,4 +77,4 @@ This script: --- -**Last generated**: 2025-11-01 +**Last generated**: 2025-11-02 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 5f78862..ae3b71a 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,7 +1,7 @@ # 🏗️ flarelette-jwt-kit **Architecture Documentation** -Generated 2025-11-01 16:48:43 +Generated 2025-11-02 06:03:12 ## Overview diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt__adapters.md b/docs/architecture/chrislyons_dev_flarelette_jwt__adapters.md index a4aaee2..38a08f2 100644 --- a/docs/architecture/chrislyons_dev_flarelette_jwt__adapters.md +++ b/docs/architecture/chrislyons_dev_flarelette_jwt__adapters.md @@ -121,7 +121,7 @@ Automatically injects JWKS service binding if configured.
{ sign: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/sign").sign; verify: (token: string, opts?: Partial<{ iss: string; aud: string; leeway: number; }>) => Promise; createToken: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high").createToken; checkAuth: (token: string, opts?: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high").AuthzOpts) => Promise; policy: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high").policy; parse: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/util").parse; isExpiringSoon: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/util").isExpiringSoon; } { sign: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/sign").sign; verify: (token: string, opts?: Partial<{ iss: string; aud: string; leeway: number; }>) => Promise; createToken: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high").createToken; checkAuth: (token: string, opts?: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high").AuthzOpts) => Promise; policy: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high").policy; parse: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/util").parse; isExpiringSoon: typeof import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/util").isExpiringSoon; } import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").ClaimsDict — - Claims to include in the token (can include custom claims beyond standard JWT fields)- `opts`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional overrides for iss, aud, ttlSeconds
+- `payload`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").JwtPayload — - Claims to include in the token (can include custom claims beyond standard JWT fields)- `opts`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional overrides for iss, aud, ttlSeconds
---
diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt__util.md b/docs/architecture/chrislyons_dev_flarelette_jwt__util.md
index cfa16a3..754cdbe 100644
--- a/docs/architecture/chrislyons_dev_flarelette_jwt__util.md
+++ b/docs/architecture/chrislyons_dev_flarelette_jwt__util.md
@@ -83,14 +83,14 @@ Create a signed JWT token with optional claims
C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts:19C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts:18import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").ClaimsDict — - Claims to include in the token (can include custom claims beyond standard JWT fields)- `opts`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional overrides for iss, aud, ttlSeconds
+- `claims`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").JwtPayload — - Claims to include in the token (can include custom claims beyond standard JWT fields)- `opts`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional overrides for iss, aud, ttlSeconds
---
##### `createDelegatedToken()`
@@ -125,14 +125,14 @@ Pattern: "I'm C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts:62C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts:61import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").ClaimsDict — - The verified JWT payload from external auth (e.g., Auth0)- `actorService`: string — - Identifier of the service creating this delegated token- `opts`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional overrides for iss, aud, ttlSeconds
+- `originalPayload`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").JwtPayload — - The verified JWT payload from external auth (e.g., Auth0)- `actorService`: string — - Identifier of the service creating this delegated token- `opts`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional overrides for iss, aud, ttlSeconds
**Examples:**
```typescript
@@ -163,7 +163,7 @@ Verify and authorize a JWT token with policy enforcement
C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts:149C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts:142{ base(b: Partial<{ iss: string; aud: string | string[]; leeway: number; }>): any; needAll(...perms: string[]): any; needAny(...perms: string[]): any; rolesAll(...roles: string[]): any; rolesAny(...roles: string[]): any; where(fn: (payload: JWTPayload) => boolean): any; build(): import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high").AuthzOpts; } — Policy builder with chainable methods{ base(b: Partial<{ iss: string; aud: string | string[]; leeway: number; }>): any; needAll(...perms: string[]): any; needAny(...perms: string[]): any; rolesAll(...roles: string[]): any; rolesAny(...roles: string[]): any; where(fn: (payload: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").JwtPayload) => boolean): any; build(): import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high").AuthzOpts; } — Policy builder with chainable methodsC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts:184C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts:177Promise — Decoded payload if valid, null otherwisePromise — Decoded payload if valid, null otherwiseC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts:29C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts:28C:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\high.py:217C:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\high.py:227dict — Custom claims to include in the token
+- `claims`: JwtPayload — Claims to include in the token (can include custom claims beyond standard JWT fields)
---
##### `create_delegated_token()`
@@ -496,7 +496,7 @@ See Also:
**Parameters:**
-- `original_payload`: dict[str, JwtValue] — The verified JWT payload from external auth (e.g., Auth0)- `actor_service`: str — Identifier of the service creating this delegated token
+- `original_payload`: JwtPayload — The verified JWT payload from external auth (e.g., Auth0)- `actor_service`: str — Identifier of the service creating this delegated token
**Examples:**
```typescript
@@ -527,7 +527,7 @@ Verify and authorize a JWT token with policy enforcement.
C:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\high.py:156C:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\high.py:166C:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\high.py:209C:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\high.py:219dict — Claims to include in the token
+- `payload`: JwtPayload — Claims to include in the token (can include custom claims beyond standard JWT fields)
---
##### `parse()`
diff --git a/packages/flarelette-jwt-py/flarelette_jwt/__init__.py b/packages/flarelette-jwt-py/flarelette_jwt/__init__.py
index 9bf3e60..18e3e76 100644
--- a/packages/flarelette-jwt-py/flarelette_jwt/__init__.py
+++ b/packages/flarelette-jwt-py/flarelette_jwt/__init__.py
@@ -8,7 +8,6 @@
from .env import (
ActorClaim,
AlgType,
- ClaimsDict,
JwtCommonConfig,
JwtHeader,
JwtPayload,
@@ -30,7 +29,6 @@
# Types
"AlgType",
"JwtValue",
- "ClaimsDict",
"JwtProfile",
"JwtCommonConfig",
"JwtHeader",
diff --git a/packages/flarelette-jwt-py/flarelette_jwt/high.py b/packages/flarelette-jwt-py/flarelette_jwt/high.py
index 1b04142..30e4234 100644
--- a/packages/flarelette-jwt-py/flarelette_jwt/high.py
+++ b/packages/flarelette-jwt-py/flarelette_jwt/high.py
@@ -18,7 +18,7 @@
if TYPE_CHECKING:
from collections.abc import Callable
- from .env import JwtPayload, JwtValue
+ from .env import JwtPayload
class AuthUser(TypedDict, total=False):
@@ -51,12 +51,12 @@ def need_all(self, *p: str) -> PolicyBuilder: ...
def need_any(self, *p: str) -> PolicyBuilder: ...
def roles_all(self, *r: str) -> PolicyBuilder: ...
def roles_any(self, *r: str) -> PolicyBuilder: ...
- def where(self, fn: Callable[[dict[str, JwtValue]], bool]) -> PolicyBuilder: ...
+ def where(self, fn: Callable[[JwtPayload], bool]) -> PolicyBuilder: ...
def build(self) -> dict[str, Any]: ...
async def create_token(
- claims: dict,
+ claims: JwtPayload,
*,
iss: str | None = None,
aud: str | list[str] | None = None,
@@ -65,7 +65,7 @@ async def create_token(
"""Create a signed JWT token with optional claims.
Args:
- claims: Custom claims to include in the token
+ claims: Claims to include in the token (can include custom claims beyond standard JWT fields)
iss: Optional issuer override
aud: Optional audience override (string or list)
ttl_seconds: Optional TTL override in seconds
@@ -77,7 +77,7 @@ async def create_token(
async def create_delegated_token(
- original_payload: dict[str, JwtValue],
+ original_payload: JwtPayload,
actor_service: str,
*,
iss: str | None = None,
@@ -127,7 +127,7 @@ async def create_delegated_token(
- security.md: Service Delegation Pattern section
"""
# Preserve original user context and permissions
- delegated_claims: dict[str, JwtValue] = {
+ delegated_claims: dict[str, Any] = {
"sub": original_payload.get("sub"), # Original end user
"permissions": original_payload.get("permissions", []), # NO escalation
"roles": original_payload.get("roles", []),
@@ -146,11 +146,21 @@ async def create_delegated_token(
delegated_claims["act"] = {"sub": actor_service}
# Preserve additional context fields if present
- for field in ["email", "name", "groups", "tid", "org_id", "department"]:
- if field in original_payload:
- delegated_claims[field] = original_payload[field]
-
- return await sign(delegated_claims, iss=iss, aud=aud, ttl_seconds=ttl_seconds)
+ if original_payload.get("email"):
+ delegated_claims["email"] = original_payload["email"]
+ if original_payload.get("name"):
+ delegated_claims["name"] = original_payload["name"]
+ if original_payload.get("groups"):
+ delegated_claims["groups"] = original_payload["groups"]
+ if original_payload.get("tid"):
+ delegated_claims["tid"] = original_payload["tid"]
+ if original_payload.get("org_id"):
+ delegated_claims["org_id"] = original_payload["org_id"]
+ if original_payload.get("department"):
+ delegated_claims["department"] = original_payload["department"]
+
+ # Type cast to JwtPayload for type checking - safe because we control the structure
+ return await sign(delegated_claims, iss=iss, aud=aud, ttl_seconds=ttl_seconds) # type: ignore[arg-type]
async def check_auth(
@@ -163,7 +173,7 @@ async def check_auth(
require_any_permission: list[str] | None = None,
require_roles_all: list[str] | None = None,
require_roles_any: list[str] | None = None,
- predicates: list[Callable[[dict[str, JwtValue]], bool]] | None = None,
+ predicates: list[Callable[[JwtPayload], bool]] | None = None,
) -> AuthUser | None:
"""Verify and authorize a JWT token with policy enforcement.
@@ -196,7 +206,7 @@ async def check_auth(
return None
if predicates:
for fn in predicates:
- if not fn(payload): # type: ignore[arg-type]
+ if not fn(payload):
return None
return {
"sub": payload.get("sub"),
@@ -239,7 +249,7 @@ def roles_any(self, *r: str) -> PolicyBuilder:
opts["require_roles_any"].extend(r)
return self
- def where(self, fn: Callable[[dict[str, JwtValue]], bool]) -> PolicyBuilder:
+ def where(self, fn: Callable[[JwtPayload], bool]) -> PolicyBuilder:
opts.setdefault("predicates", [])
opts["predicates"].append(fn)
return self
diff --git a/packages/flarelette-jwt-py/flarelette_jwt/sign.py b/packages/flarelette-jwt-py/flarelette_jwt/sign.py
index d60cdae..9e13553 100644
--- a/packages/flarelette-jwt-py/flarelette_jwt/sign.py
+++ b/packages/flarelette-jwt-py/flarelette_jwt/sign.py
@@ -15,7 +15,7 @@
import time
# NOTE: 'js' module imported lazily inside functions - only available in Cloudflare Workers
-from .env import AlgType, common, get_hs_secret_bytes, mode
+from .env import AlgType, JwtPayload, common, get_hs_secret_bytes, mode
def _b64url(b: bytes) -> str:
@@ -23,7 +23,7 @@ def _b64url(b: bytes) -> str:
async def sign(
- payload: dict,
+ payload: JwtPayload,
*,
iss: str | None = None,
aud: str | list[str] | None = None,
@@ -32,7 +32,7 @@ async def sign(
"""Sign a JWT token with HS512 or EdDSA algorithm.
Args:
- payload: Claims to include in the token
+ payload: Claims to include in the token (can include custom claims beyond standard JWT fields)
iss: Optional issuer override
aud: Optional audience override (string or list)
ttl_seconds: Optional TTL override in seconds
diff --git a/packages/flarelette-jwt-py/tests/test_sign_verify_mocked.py b/packages/flarelette-jwt-py/tests/test_sign_verify_mocked.py
index 4f7e3ec..f0d89e7 100644
--- a/packages/flarelette-jwt-py/tests/test_sign_verify_mocked.py
+++ b/packages/flarelette-jwt-py/tests/test_sign_verify_mocked.py
@@ -7,13 +7,15 @@
from __future__ import annotations
import os
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, cast
import pytest
if TYPE_CHECKING:
from collections.abc import Generator
+ from flarelette_jwt import JwtPayload
+
# Install js mock BEFORE importing anything from flarelette_jwt
from .mock_js import install_js_mock, uninstall_js_mock
@@ -57,7 +59,9 @@ def setup_env(self) -> Generator[None, None, None]:
@pytest.mark.asyncio
async def test_sign_creates_token(self) -> None:
"""Should sign a token with HS512."""
- payload = {"sub": "123", "permissions": ["test@example.com"]}
+ payload = cast(
+ "JwtPayload", {"sub": "123", "permissions": ["test@example.com"]}
+ )
token = await sign(payload)
@@ -68,7 +72,7 @@ async def test_sign_creates_token(self) -> None:
@pytest.mark.asyncio
async def test_sign_includes_claims(self) -> None:
"""Should include standard JWT claims."""
- payload = {"sub": "123"}
+ payload = cast("JwtPayload", {"sub": "123"})
token = await sign(payload)
verified = await verify(token)
@@ -83,7 +87,7 @@ async def test_sign_includes_claims(self) -> None:
@pytest.mark.asyncio
async def test_verify_accepts_valid_token(self) -> None:
"""Should verify a valid token."""
- payload = {"sub": "456", "roles": ["admin"]}
+ payload = cast("JwtPayload", {"sub": "456", "roles": ["admin"]})
token = await sign(payload)
verified = await verify(token)
@@ -104,7 +108,7 @@ async def test_verify_rejects_invalid_token(self) -> None:
@pytest.mark.asyncio
async def test_verify_with_custom_options(self) -> None:
"""Should accept custom issuer and audience."""
- payload = {"permissions": ["test"]}
+ payload = cast("JwtPayload", {"permissions": ["test"]})
token = await sign(payload)
# Verify with same issuer/audience
@@ -116,7 +120,7 @@ async def test_verify_with_custom_options(self) -> None:
@pytest.mark.asyncio
async def test_sign_with_custom_ttl(self) -> None:
"""Should sign with custom TTL."""
- payload = {"sub": "789"}
+ payload = cast("JwtPayload", {"sub": "789"})
token = await sign(payload, ttl_seconds=7200)
verified = await verify(token)
@@ -128,7 +132,7 @@ async def test_sign_with_custom_ttl(self) -> None:
@pytest.mark.asyncio
async def test_create_token_basic(self) -> None:
"""Should create token with basic payload."""
- token = await create_token({"sub": "123"})
+ token = await create_token(cast("JwtPayload", {"sub": "123"}))
verified = await verify(token)
@@ -139,7 +143,7 @@ async def test_create_token_basic(self) -> None:
async def test_create_token_with_options(self) -> None:
"""Should create token with custom options."""
token = await create_token(
- {"sub": "123"}, iss="custom-issuer", ttl_seconds=7200
+ cast("JwtPayload", {"sub": "123"}), iss="custom-issuer", ttl_seconds=7200
)
verified = await verify(token, iss="custom-issuer")
@@ -152,9 +156,9 @@ async def test_create_token_with_options(self) -> None:
async def test_multiple_sign_verify_cycles(self) -> None:
"""Should handle multiple sign/verify operations."""
payloads = [
- {"sub": "1", "roles": ["Alice"]},
- {"sub": "2", "roles": ["Bob"]},
- {"sub": "3", "roles": ["Charlie"]},
+ cast("JwtPayload", {"sub": "1", "roles": ["Alice"]}),
+ cast("JwtPayload", {"sub": "2", "roles": ["Bob"]}),
+ cast("JwtPayload", {"sub": "3", "roles": ["Charlie"]}),
]
for payload in payloads:
diff --git a/packages/flarelette-jwt-ts/package.json b/packages/flarelette-jwt-ts/package.json
index 86d98bf..93857d0 100644
--- a/packages/flarelette-jwt-ts/package.json
+++ b/packages/flarelette-jwt-ts/package.json
@@ -1,6 +1,6 @@
{
"name": "@chrislyons-dev/flarelette-jwt",
- "version": "1.8.2",
+ "version": "1.8.3",
"type": "module",
"description": "Environment-driven JWT authentication for Cloudflare Workers with secret-name indirection",
"keywords": [
diff --git a/packages/flarelette-jwt-ts/src/high.ts b/packages/flarelette-jwt-ts/src/high.ts
index 11a7ad7..0a875f4 100644
--- a/packages/flarelette-jwt-ts/src/high.ts
+++ b/packages/flarelette-jwt-ts/src/high.ts
@@ -6,8 +6,7 @@
*/
import { sign } from './sign.js'
import { verify } from './verify.js'
-import type { ClaimsDict, Fetcher } from './types.js'
-import type { JWTPayload } from 'jose'
+import type { Fetcher, JwtPayload } from './types.js'
/**
* Create a signed JWT token with optional claims
@@ -17,7 +16,7 @@ import type { JWTPayload } from 'jose'
* @returns Signed JWT token string
*/
export async function createToken(
- claims: ClaimsDict,
+ claims: JwtPayload,
opts?: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number }>
) {
return sign(claims, opts)
@@ -60,12 +59,12 @@ export async function createToken(
* @see security.md - Service Delegation Pattern section
*/
export async function createDelegatedToken(
- originalPayload: ClaimsDict,
+ originalPayload: JwtPayload,
actorService: string,
opts?: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number }>
): Promise