PR: Add workflow to include billing info in user tokens - B2B case#23
PR: Add workflow to include billing info in user tokens - B2B case#23ahn-nath wants to merge 4 commits into
Conversation
A new file was added with a workflow that can be used to extend the access and ID tokens with billing information for Business to Business models.
📝 WalkthroughWalkthroughAdds a B2B token enrichment workflow that validates an organization code, fetches billing_customer_id, retrieves entitlements and agreements (parallel/conditional), builds an Changes
Sequence DiagramsequenceDiagram
participant Event as Event/Caller
participant Workflow as OrganizationBillingWorkflow
participant KindeAPI as Kinde API
participant Token as Token Claims
Event->>Workflow: Trigger with WorkflowEvent
Note over Workflow: Validate org code (early exit if missing)
Workflow->>KindeAPI: Fetch organization (expand billing)
KindeAPI-->>Workflow: Organization + billing_customer_id
Note over Workflow: Exit if no customer_id
rect rgba(200,220,240,0.5)
par Fetch entitlements
Workflow->>KindeAPI: Fetch entitlements (expand plans)
KindeAPI-->>Workflow: Entitlements[]
and Fetch agreements (conditional)
Workflow->>KindeAPI: Fetch agreements (if not present)
KindeAPI-->>Workflow: Agreements[]
end
end
Note over Workflow: Assemble org_billing claim (customer_id, entitlements, agreements)
rect rgba(220,240,220,0.5)
Workflow->>Token: Inject org_billing into accessToken
Workflow->>Token: Inject org_billing into idToken
end
Token-->>Event: Tokens with org_billing claim
Note over Workflow: Log success or error (rethrow on failure)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
userTokens/addBillingDetailsToTokensB2BWorkflow.ts (2)
172-176: Error logging: consider logging the stack (when available).
Current log prints.message(Line 174) but dropsstack, which is typically what you’ll need to debug workflow failures.- console.error("B2B Workflow Error:", (err as Error).message ?? err); + const e = err as Error; + console.error("B2B Workflow Error:", e?.stack ?? e?.message ?? err); throw err;
1-20: Bindings pattern is standard Kinde workflow practice; consider clarifying with a comment.The bindings declared as
"kinde.accessToken"and"kinde.idToken"inworkflowSettings.bindingsare made available by the Kinde workflow runtime as typed global identifiers (kinde.accessToken,kinde.idToken, etc.). This is the standard pattern across all workflow files in this repository. However, the mapping from string binding keys to runtime identifiers is implicit. Consider adding a clarifying comment (e.g.,// kinde bindings injected by workflow runtime) at the top of the implementation block wherekinde.accessToken.setCustomClaim()is called to improve code clarity for future maintainers.
| // [4] Set Custom Claims | ||
| // We inject this data into the tokens so the frontend/API can enforce limits immediately. | ||
| kinde.accessToken.setCustomClaim("org_billing", billingClaimObject); | ||
| kinde.idToken.setCustomClaim("org_billing", billingClaimObject); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the structure of the repository to locate the file
find . -name "addBillingDetailsToTokensB2BWorkflow.ts" -type fRepository: kinde-starter-kits/workflow-examples
Length of output: 132
🏁 Script executed:
# Read the file to check imports and the context around lines 165-169
cat -n ./userTokens/addBillingDetailsToTokensB2BWorkflow.ts | head -180Repository: kinde-starter-kits/workflow-examples
Length of output: 7011
🏁 Script executed:
# Search for other usages of setCustomClaim or token binding patterns in the repo
find . -type f -name "*.ts" | xargs grep -l "setCustomClaim\|kinde\.accessToken\|kinde\.idToken" 2>/dev/null | head -5Repository: kinde-starter-kits/workflow-examples
Length of output: 225
🏁 Script executed:
# Check one of the other files to see how tokens are properly used
cat -n ./userTokens/customClaimsAccessTokenWorkflow.ts | head -120Repository: kinde-starter-kits/workflow-examples
Length of output: 1973
🏁 Script executed:
# Check the B2C file to see if it has similar pattern issues
cat -n ./userTokens/addBillingDetailsToTokensB2C.ts | head -150Repository: kinde-starter-kits/workflow-examples
Length of output: 5515
🏁 Script executed:
# Read the rest of the B2C file to see how tokens are used
cat -n ./userTokens/addBillingDetailsToTokensB2C.ts | tail -50Repository: kinde-starter-kits/workflow-examples
Length of output: 2140
🏁 Script executed:
# Search for any documentation or README files that might explain the workflow pattern
find . -type f \( -name "README*" -o -name "*.md" \) | xargs grep -l "setCustomClaim\|accessToken\|idToken" 2>/dev/null | head -3Repository: kinde-starter-kits/workflow-examples
Length of output: 62
🏁 Script executed:
# Search for any usage of idTokenCustomClaims or token claim patterns
grep -r "idTokenCustomClaims\|accessTokenCustomClaims" . --include="*.ts" --include="*.js"Repository: kinde-starter-kits/workflow-examples
Length of output: 250
🏁 Script executed:
# Check package.json for @kinde/infrastructure version and details
cat package.json | grep -A 5 -B 5 "@kinde/infrastructure"Repository: kinde-starter-kits/workflow-examples
Length of output: 223
🌐 Web query:
@kinde/infrastructure 0.4.1 API documentation token claims accessToken idToken
💡 Result:
Summary for @kinde/infrastructure (v0.4.1) — token claims (accessToken / idToken)
-
Library exposes type-safe helpers to add custom claims inside workflows:
- accessTokenCustomClaims() — returns an object you assign custom access-token claims to. [1][2]
- idTokenCustomClaims() — returns an object you assign custom ID-token claims to. [1][2]
-
Low-level workflow bindings (alternative to the helpers):
- kinde.accessToken.setCustomClaim(name, value) — add claim to access token. [2]
- kinde.idToken.setCustomClaim(name, value) — add claim to ID token. [3]
-
Client SDK helpers to read claims:
- getClaim(claim: string, tokenKey?: "accessToken" | "idToken") — reads a claim from access or ID token (defaults to accessToken). [4]
-
Notes:
- Some standard/prohibited claims cannot be modified from a workflow (see Kinde docs / prohibited claims). [1][2]
- Avoid adding sensitive data to tokens (tokens are client-visible). [1][5]
Sources:
[1] Kinde docs — access-token binding / usage with @kinde/infrastructure.
[2] Kinde docs — access-token binding page.
[3] Kinde docs — id-token binding page.
[4] Kinde React SDK docs — getClaim API.
[5] npm: @kinde/infrastructure package README (examples showing accessToken/idToken helpers).
Replace direct token binding access with @kinde/infrastructure helper functions.
Lines 167-168 use kinde.accessToken.setCustomClaim() and kinde.idToken.setCustomClaim(), but this pattern is not how workflows properly wire token objects in @kinde/infrastructure v0.4.1. Use the typed helper functions instead:
import { accessTokenCustomClaims, idTokenCustomClaims } from "@kinde/infrastructure";
// Later in your workflow:
const accessToken = accessTokenCustomClaims<OrgBillingClaim>();
Object.assign(accessToken, billingClaimObject);
const idToken = idTokenCustomClaims<OrgBillingClaim>();
Object.assign(idToken, billingClaimObject);Additionally, embedding full entitlements/agreements arrays into both tokens will bloat JWTs. Consider injecting only IDs/codes and version hashes, or place the full payload in only the access token.
🤖 Prompt for AI Agents
In userTokens/addBillingDetailsToTokensB2BWorkflow.ts around lines 165 to 169,
replace the direct token binding calls kinde.accessToken.setCustomClaim(...) and
kinde.idToken.setCustomClaim(...) with the typed helpers from
@kinde/infrastructure: import accessTokenCustomClaims and idTokenCustomClaims,
create typed claim objects via those helpers and merge billingClaimObject into
them (e.g., Object.assign), and avoid embedding full entitlements/agreements
arrays into both tokens — instead include only IDs/codes or version hashes, or
keep the full payload only on the access token to prevent JWT bloat.
|
Hi @ahn-nath, One thing I wanted to flag for awareness around typing usage: I noticed generic type parameters being used in several places such as It might be worth either removing the generics and typing the response after the call, or adding a brief comment to clarify that the generics are for developer clarity rather than enforced typing. Nothing blocking here, and amazing job on this! |
|
@Arobce |
|
Hello, Roshan. Again, thank you very much for your feedback. I have decided to remove the typing because this aligns better with TypeScript principles. I sent my work for review. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@userTokens/addBillingDetailsToTokensB2BWorkflow.ts`:
- Around line 155-157: The expression assigning agreements contains a stray '+'
character before agrResp which causes a syntax error; remove the leading '+' so
the ternary uses agrResp?.data?.agreements and call
ensureArray<Agreement>(agrResp.data.agreements) in the true branch, otherwise
fall back to initialAgreements (refer to the agreements variable, agrResp,
initialAgreements, and ensureArray<Agreement> to locate the line in
addBillingDetailsToTokensB2BWorkflow.ts).
- Around line 35-44: The OrganizationResponse type is out of sync with the real
API: update the billing shape in the OrganizationResponse interface (symbol:
OrganizationResponse) to include billing_customer_id?: string (instead of or in
addition to customer_id?) so TypeScript matches the actual Kinde API field; then
ensure code paths that read billing_customer_id (e.g., the access in the
workflow that currently reads billing_customer_id) are typed correctly by this
updated interface.
🧹 Nitpick comments (1)
userTokens/addBillingDetailsToTokensB2BWorkflow.ts (1)
143-153: Inconsistent typing approach - generic still present on line 150.The PR objective states generics were removed and responses typed explicitly. Line 146 correctly uses a type assertion (
as Promise<{ data: EntitlementsResponse }>), but line 150 still uses a generic (kindeAPI.get<AgreementsResponse>). Apply the same pattern for consistency.♻️ Proposed fix for consistent typing
const [entResp, agrResp] = await Promise.all([ kindeAPI.get({ endpoint: `billing/entitlements?customer_id=${customerId}&expand=plans` }) as Promise<{ data: EntitlementsResponse }>, // Only call agreements if we could not get it from the previous call initialAgreements.length === 0 - ? kindeAPI.get<AgreementsResponse>({ + ? kindeAPI.get({ endpoint: `billing/agreements?customer_id=${customerId}` - }): Promise.resolve(null) + }) as Promise<{ data: AgreementsResponse }> : Promise.resolve(null) ]);
| const entitlements = ensureArray<Entitlement>(entResp?.data?.entitlements); | ||
| const agreements = | ||
| + agrResp?.data?.agreements ? ensureArray<Agreement>(agrResp.data.agreements) : initialAgreements; |
There was a problem hiding this comment.
Stray + character will cause a syntax error.
Line 157 has a + character at the beginning of the expression which is syntactically invalid and will prevent the code from compiling.
🐛 Proposed fix
const entitlements = ensureArray<Entitlement>(entResp?.data?.entitlements);
const agreements =
-+ agrResp?.data?.agreements ? ensureArray<Agreement>(agrResp.data.agreements) : initialAgreements;
+ agrResp?.data?.agreements ? ensureArray<Agreement>(agrResp.data.agreements) : initialAgreements;🤖 Prompt for AI Agents
In `@userTokens/addBillingDetailsToTokensB2BWorkflow.ts` around lines 155 - 157,
The expression assigning agreements contains a stray '+' character before
agrResp which causes a syntax error; remove the leading '+' so the ternary uses
agrResp?.data?.agreements and call
ensureArray<Agreement>(agrResp.data.agreements) in the true branch, otherwise
fall back to initialAgreements (refer to the agreements variable, agrResp,
initialAgreements, and ensureArray<Agreement> to locate the line in
addBillingDetailsToTokensB2BWorkflow.ts).
Explain your changes
Summary
Adds a B2B-specific workflow that fetches the active Organization’s billing information during token generation and adds it to the user tokens as an
org_billingcustom claim.The workflow validates that an organization context exists, fetches the organization's billing profile via the Kinde Management API, retrieves entitlements and agreements in parallel, and gracefully handles errors or missing contexts.
Files changed
addBillingDetailsToTokensB2B.ts(new workflow implementation)What this implements
user:tokens_generationevent and sets theorg_billingclaim on both access and ID tokens.event.context.organization.codeexists.GET /organization?code={code}&expand=billing(to get the Org'scustomer_id)GET /billing/entitlements?customer_id=...GET /billing/agreements?customer_id=...OrganizationResponse,OrgBillingClaim,Entitlement, andAgreement.Promise.all) for entitlements and agreements to minimize latency (<2s execution target).Notes & follow-ups
Checklist
🛟 If you need help, consider asking for advice over in the [Kinde community](https://thekindecommunity.slack.com).
Summary by CodeRabbit
New Features
org_billingcustom claim to access and ID tokens when a user signs into an Organization.Performance
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.