@tdacorp/identity-client 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,6 +53,9 @@ export const GET = createCallbackRoute({
53
53
  redirectUri: `${process.env.APP_URL}/api/auth/callback`,
54
54
  cookieSecret: process.env.COOKIE_SECRET!,
55
55
  onSuccess: async (tokens, idClaims, { returnTo }) => {
56
+ // returnTo, when present, has already been validated to a same-app
57
+ // relative path (single leading `/`, never an absolute URL) by
58
+ // createLoginRoute -- safe to resolve against APP_URL like this.
56
59
  const response = NextResponse.redirect(new URL(returnTo ?? '/', process.env.APP_URL))
57
60
  // Store tokens.accessToken / tokens.refreshToken in your own session --
58
61
  // this package stops at verified claims, session storage is your call.
@@ -61,6 +64,18 @@ export const GET = createCallbackRoute({
61
64
  })
62
65
  ```
63
66
 
67
+ `createLoginRoute` rejects anything but a relative path for `returnTo` --
68
+ an absolute URL, a protocol-relative `//host` value, a backslash, or a
69
+ control character is silently dropped (coerced to `undefined`) rather than
70
+ sealed into the transaction cookie, closing the open-redirect this shape
71
+ would otherwise allow. This validation applies regardless of whether
72
+ `returnTo` came from the default `returnTo` search param or a custom
73
+ `getReturnTo`, since this package has no per-app trusted-origin registry to
74
+ check an absolute URL's origin against. If your app legitimately needs to
75
+ redirect to an absolute URL after login, carry it through your own
76
+ mechanism (a separate cookie your app validates against its own known
77
+ origins) rather than this package's `returnTo`.
78
+
64
79
  Both handlers seal the PKCE verifier and CSRF `state` into an encrypted
65
80
  cookie (via `./sealed`'s `seal()`/`unseal()`) rather than storing them in
66
81
  plaintext — you never handle that transaction cookie directly.
@@ -119,10 +134,11 @@ const discovery = await fetchDiscovery('https://identity.tdacorp.in')
119
134
  deduplication — concurrent callers for the same issuer during a cold start
120
135
  share one fetch rather than each firing their own. Before the result is
121
136
  trusted, it is validated: the document's `issuer` must match the URL you
122
- called with, and `jwks_uri` / `token_endpoint` must be https (loopback hosts
123
- excepted) and same-origin with the issuer. This is what stops a compromised
124
- edge cache or a cached error page from redirecting key material or the token
125
- endpoint somewhere untrusted.
137
+ called with, and `jwks_uri` / `token_endpoint` / `authorization_endpoint`
138
+ must be https (loopback hosts excepted) and same-origin with the issuer.
139
+ This is what stops a compromised edge cache or a cached error page from
140
+ redirecting key material, the token endpoint, or the login page itself
141
+ somewhere untrusted.
126
142
 
127
143
  Most callers reach `fetchDiscovery` indirectly — every function below calls
128
144
  it internally.
@@ -199,11 +215,13 @@ if (outcome.outcome === 'success') {
199
215
  Machine-to-machine calls with no end user use `clientCredentialsGrant` (RFC
200
216
  6749 §4.4) instead, always with a confidential `clientSecret` since there is
201
217
  no PKCE verifier to authenticate a public client with. A token minted this
202
- way carries no `sub` and no `roles` claim — verifying it with
203
- `verifyAccessToken` returns the `kind: 'machine'` variant of
204
- `VerifiedAccessToken`, distinct from the `kind: 'user'` variant a normal
205
- login produces, so reading `.sub` or `.roles` off the wrong branch is a
206
- compile error rather than `undefined`.
218
+ way carries no `sub` — verifying it with `verifyAccessToken` returns the
219
+ `kind: 'machine'` variant of `VerifiedAccessToken`, distinct from the
220
+ `kind: 'user'` variant a normal login produces, so reading `.sub` off the
221
+ wrong branch is a compile error rather than `undefined`. It can still carry a
222
+ `roles` claim: request `roles` in the client_credentials scope and the server
223
+ resolves it against the requesting client itself as the subject, exactly as
224
+ it does for a user token requesting the same scope.
207
225
 
208
226
  ## Verification
209
227
 
@@ -35,6 +35,7 @@ function validateDiscoveryDocument(document, issuerUrl) {
35
35
  assertHttps(issuerUrl, "issuer");
36
36
  assertHttps(document.jwks_uri, "jwks_uri");
37
37
  assertHttps(document.token_endpoint, "token_endpoint");
38
+ assertHttps(document.authorization_endpoint, "authorization_endpoint");
38
39
  if (document.issuer !== issuerUrl) {
39
40
  throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
40
41
  }
@@ -45,6 +46,9 @@ function validateDiscoveryDocument(document, issuerUrl) {
45
46
  if (new URL(document.token_endpoint).origin !== issuerOrigin) {
46
47
  throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
47
48
  }
49
+ if (new URL(document.authorization_endpoint).origin !== issuerOrigin) {
50
+ throw new Error(`Discovery: authorization_endpoint "${document.authorization_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
51
+ }
48
52
  }
49
53
  async function fetchDiscovery(issuerUrl) {
50
54
  const cached = discoveryCache.get(issuerUrl);
@@ -322,7 +326,7 @@ async function verifyAccessToken(accessToken, options) {
322
326
  errors: [{ code: "malformed-claims", message: "Access token has neither a sub (user) nor an azp (machine client id)" }]
323
327
  };
324
328
  }
325
- return { success: true, data: { kind: "machine", clientId: payload.azp, claims } };
329
+ return { success: true, data: { kind: "machine", clientId: payload.azp, roles, claims } };
326
330
  }
327
331
 
328
332
  export {
package/dist/index.cjs CHANGED
@@ -72,6 +72,7 @@ function validateDiscoveryDocument(document, issuerUrl) {
72
72
  assertHttps(issuerUrl, "issuer");
73
73
  assertHttps(document.jwks_uri, "jwks_uri");
74
74
  assertHttps(document.token_endpoint, "token_endpoint");
75
+ assertHttps(document.authorization_endpoint, "authorization_endpoint");
75
76
  if (document.issuer !== issuerUrl) {
76
77
  throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
77
78
  }
@@ -82,6 +83,9 @@ function validateDiscoveryDocument(document, issuerUrl) {
82
83
  if (new URL(document.token_endpoint).origin !== issuerOrigin) {
83
84
  throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
84
85
  }
86
+ if (new URL(document.authorization_endpoint).origin !== issuerOrigin) {
87
+ throw new Error(`Discovery: authorization_endpoint "${document.authorization_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
88
+ }
85
89
  }
86
90
  async function fetchDiscovery(issuerUrl) {
87
91
  const cached = discoveryCache.get(issuerUrl);
@@ -359,7 +363,7 @@ async function verifyAccessToken(accessToken, options) {
359
363
  errors: [{ code: "malformed-claims", message: "Access token has neither a sub (user) nor an azp (machine client id)" }]
360
364
  };
361
365
  }
362
- return { success: true, data: { kind: "machine", clientId: payload.azp, claims } };
366
+ return { success: true, data: { kind: "machine", clientId: payload.azp, roles, claims } };
363
367
  }
364
368
  // Annotate the CommonJS export names for ESM import in node:
365
369
  0 && (module.exports = {
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRemoteJWKSet } from 'jose';
2
- export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as TokenEndpointAuthMethod, b as TokenRefreshOutcome, c as TokenRequestError, d as TokenSet, e as TokenVerificationError, f as TokenVerificationErrorCode, V as VerificationResult, g as VerifiedAccessToken, h as VerifyTokenOptions, i as clientCredentialsGrant, j as exchangeAuthorizationCode, r as refreshTokens, v as verifyAccessToken, k as verifyIdToken } from './verify-DTUDjR4v.cjs';
2
+ export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as TokenEndpointAuthMethod, b as TokenRefreshOutcome, c as TokenRequestError, d as TokenSet, e as TokenVerificationError, f as TokenVerificationErrorCode, V as VerificationResult, g as VerifiedAccessToken, h as VerifyTokenOptions, i as clientCredentialsGrant, j as exchangeAuthorizationCode, r as refreshTokens, v as verifyAccessToken, k as verifyIdToken } from './verify-C1Ga6JbS.cjs';
3
3
  import '@tdacorp/identity-authz';
4
4
 
5
5
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRemoteJWKSet } from 'jose';
2
- export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as TokenEndpointAuthMethod, b as TokenRefreshOutcome, c as TokenRequestError, d as TokenSet, e as TokenVerificationError, f as TokenVerificationErrorCode, V as VerificationResult, g as VerifiedAccessToken, h as VerifyTokenOptions, i as clientCredentialsGrant, j as exchangeAuthorizationCode, r as refreshTokens, v as verifyAccessToken, k as verifyIdToken } from './verify-DTUDjR4v.js';
2
+ export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as TokenEndpointAuthMethod, b as TokenRefreshOutcome, c as TokenRequestError, d as TokenSet, e as TokenVerificationError, f as TokenVerificationErrorCode, V as VerificationResult, g as VerifiedAccessToken, h as VerifyTokenOptions, i as clientCredentialsGrant, j as exchangeAuthorizationCode, r as refreshTokens, v as verifyAccessToken, k as verifyIdToken } from './verify-C1Ga6JbS.js';
3
3
  import '@tdacorp/identity-authz';
4
4
 
5
5
  /**
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  refreshTokens,
12
12
  verifyAccessToken,
13
13
  verifyIdToken
14
- } from "./chunk-6WGVTO7C.js";
14
+ } from "./chunk-S5WGUOJA.js";
15
15
  export {
16
16
  CLOCK_SKEW_TOLERANCE_SECONDS,
17
17
  TokenRequestError,
package/dist/next.cjs CHANGED
@@ -63,6 +63,7 @@ function validateDiscoveryDocument(document, issuerUrl) {
63
63
  assertHttps(issuerUrl, "issuer");
64
64
  assertHttps(document.jwks_uri, "jwks_uri");
65
65
  assertHttps(document.token_endpoint, "token_endpoint");
66
+ assertHttps(document.authorization_endpoint, "authorization_endpoint");
66
67
  if (document.issuer !== issuerUrl) {
67
68
  throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
68
69
  }
@@ -73,6 +74,9 @@ function validateDiscoveryDocument(document, issuerUrl) {
73
74
  if (new URL(document.token_endpoint).origin !== issuerOrigin) {
74
75
  throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
75
76
  }
77
+ if (new URL(document.authorization_endpoint).origin !== issuerOrigin) {
78
+ throw new Error(`Discovery: authorization_endpoint "${document.authorization_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
79
+ }
76
80
  }
77
81
  async function fetchDiscovery(issuerUrl) {
78
82
  const cached = discoveryCache.get(issuerUrl);
@@ -356,6 +360,17 @@ async function unseal(sealedValue, options) {
356
360
  var TRANSACTION_TTL_SECONDS = 600;
357
361
  var DEFAULT_TRANSACTION_COOKIE = "identity_oauth_txn";
358
362
  var TRANSACTION_PURPOSE = "identity-client:oauth-transaction";
363
+ function sanitizeReturnTo(returnTo) {
364
+ if (!returnTo) return void 0;
365
+ if (/[\x00-\x1f]/.test(returnTo)) return void 0;
366
+ if (returnTo.startsWith("//")) return void 0;
367
+ if (returnTo.includes("\\")) return void 0;
368
+ const lower = returnTo.toLowerCase().trim();
369
+ if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("file:")) {
370
+ return void 0;
371
+ }
372
+ return returnTo.startsWith("/") ? returnTo : void 0;
373
+ }
359
374
  function transactionCookieOptions(maxAge) {
360
375
  return {
361
376
  httpOnly: true,
@@ -370,7 +385,8 @@ function createLoginRoute(config) {
370
385
  const state = generateState();
371
386
  const codeVerifier = generateCodeVerifier();
372
387
  const codeChallenge = await generateCodeChallenge(codeVerifier);
373
- const returnTo = config.getReturnTo?.(request) ?? request.nextUrl.searchParams.get("returnTo") ?? void 0;
388
+ const rawReturnTo = config.getReturnTo?.(request) ?? request.nextUrl.searchParams.get("returnTo") ?? void 0;
389
+ const returnTo = sanitizeReturnTo(rawReturnTo);
374
390
  const transaction = { state, codeVerifier, returnTo };
375
391
  const sealedTransaction = await seal(transaction, {
376
392
  secret: config.cookieSecret,
package/dist/next.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextResponse, NextRequest } from 'next/server';
2
- import { d as TokenSet, I as IdTokenClaims } from './verify-DTUDjR4v.cjs';
2
+ import { d as TokenSet, I as IdTokenClaims } from './verify-C1Ga6JbS.cjs';
3
3
  import { SealSecret } from './sealed.cjs';
4
4
  import '@tdacorp/identity-authz';
5
5
  import 'jose';
@@ -22,7 +22,14 @@ interface CreateLoginRouteConfig {
22
22
  cookieSecret: SealSecret;
23
23
  transactionCookieName?: string;
24
24
  /** Where to send the user after login completes. Defaults to reading a
25
- * `returnTo` search param off the incoming request. */
25
+ * `returnTo` search param off the incoming request. Validated to a
26
+ * same-app relative path (single leading `/`, no `//` prefix, no
27
+ * backslash, no C0 control character, no `javascript:`/`data:`/
28
+ * `vbscript:`/`file:` scheme) before being sealed into the transaction
29
+ * cookie -- see `sanitizeReturnTo`. An invalid value is silently coerced
30
+ * to `undefined` rather than rejected with an error, since this package
31
+ * has no per-app trusted-origin registry to validate an absolute URL
32
+ * against and a bad `returnTo` shouldn't break the login flow. */
26
33
  getReturnTo?: (request: NextRequest) => string | undefined;
27
34
  }
28
35
  /** Builds a GET Route Handler that starts the OIDC login: generates PKCE and
@@ -67,7 +74,10 @@ interface CreateCallbackRouteConfig {
67
74
  * Called once the code has been exchanged and the id_token verified. This
68
75
  * package does not decide what happens next -- session creation, cookie
69
76
  * choice, redirect target are the consuming app's own concerns -- so this
70
- * callback's return value IS the route handler's response.
77
+ * callback's return value IS the route handler's response. `ctx.returnTo`,
78
+ * if present, already passed `sanitizeReturnTo`'s validation in
79
+ * `createLoginRoute`, so it is always a same-app relative path, never an
80
+ * absolute URL -- safe to pass straight into `new URL(returnTo, base)`.
71
81
  */
72
82
  onSuccess: (tokens: TokenSet, claims: IdTokenClaims, ctx: {
73
83
  returnTo?: string;
package/dist/next.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextResponse, NextRequest } from 'next/server';
2
- import { d as TokenSet, I as IdTokenClaims } from './verify-DTUDjR4v.js';
2
+ import { d as TokenSet, I as IdTokenClaims } from './verify-C1Ga6JbS.js';
3
3
  import { SealSecret } from './sealed.js';
4
4
  import '@tdacorp/identity-authz';
5
5
  import 'jose';
@@ -22,7 +22,14 @@ interface CreateLoginRouteConfig {
22
22
  cookieSecret: SealSecret;
23
23
  transactionCookieName?: string;
24
24
  /** Where to send the user after login completes. Defaults to reading a
25
- * `returnTo` search param off the incoming request. */
25
+ * `returnTo` search param off the incoming request. Validated to a
26
+ * same-app relative path (single leading `/`, no `//` prefix, no
27
+ * backslash, no C0 control character, no `javascript:`/`data:`/
28
+ * `vbscript:`/`file:` scheme) before being sealed into the transaction
29
+ * cookie -- see `sanitizeReturnTo`. An invalid value is silently coerced
30
+ * to `undefined` rather than rejected with an error, since this package
31
+ * has no per-app trusted-origin registry to validate an absolute URL
32
+ * against and a bad `returnTo` shouldn't break the login flow. */
26
33
  getReturnTo?: (request: NextRequest) => string | undefined;
27
34
  }
28
35
  /** Builds a GET Route Handler that starts the OIDC login: generates PKCE and
@@ -67,7 +74,10 @@ interface CreateCallbackRouteConfig {
67
74
  * Called once the code has been exchanged and the id_token verified. This
68
75
  * package does not decide what happens next -- session creation, cookie
69
76
  * choice, redirect target are the consuming app's own concerns -- so this
70
- * callback's return value IS the route handler's response.
77
+ * callback's return value IS the route handler's response. `ctx.returnTo`,
78
+ * if present, already passed `sanitizeReturnTo`'s validation in
79
+ * `createLoginRoute`, so it is always a same-app relative path, never an
80
+ * absolute URL -- safe to pass straight into `new URL(returnTo, base)`.
71
81
  */
72
82
  onSuccess: (tokens: TokenSet, claims: IdTokenClaims, ctx: {
73
83
  returnTo?: string;
package/dist/next.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  generateCodeVerifier,
6
6
  generateState,
7
7
  verifyIdToken
8
- } from "./chunk-6WGVTO7C.js";
8
+ } from "./chunk-S5WGUOJA.js";
9
9
  import {
10
10
  seal,
11
11
  unseal
@@ -16,6 +16,17 @@ import { NextResponse } from "next/server";
16
16
  var TRANSACTION_TTL_SECONDS = 600;
17
17
  var DEFAULT_TRANSACTION_COOKIE = "identity_oauth_txn";
18
18
  var TRANSACTION_PURPOSE = "identity-client:oauth-transaction";
19
+ function sanitizeReturnTo(returnTo) {
20
+ if (!returnTo) return void 0;
21
+ if (/[\x00-\x1f]/.test(returnTo)) return void 0;
22
+ if (returnTo.startsWith("//")) return void 0;
23
+ if (returnTo.includes("\\")) return void 0;
24
+ const lower = returnTo.toLowerCase().trim();
25
+ if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("file:")) {
26
+ return void 0;
27
+ }
28
+ return returnTo.startsWith("/") ? returnTo : void 0;
29
+ }
19
30
  function transactionCookieOptions(maxAge) {
20
31
  return {
21
32
  httpOnly: true,
@@ -30,7 +41,8 @@ function createLoginRoute(config) {
30
41
  const state = generateState();
31
42
  const codeVerifier = generateCodeVerifier();
32
43
  const codeChallenge = await generateCodeChallenge(codeVerifier);
33
- const returnTo = config.getReturnTo?.(request) ?? request.nextUrl.searchParams.get("returnTo") ?? void 0;
44
+ const rawReturnTo = config.getReturnTo?.(request) ?? request.nextUrl.searchParams.get("returnTo") ?? void 0;
45
+ const returnTo = sanitizeReturnTo(rawReturnTo);
34
46
  const transaction = { state, codeVerifier, returnTo };
35
47
  const sealedTransaction = await seal(transaction, {
36
48
  secret: config.cookieSecret,
@@ -178,12 +178,14 @@ declare function refreshTokens(options: RefreshTokensOptions): Promise<TokenRefr
178
178
  * read as "no roles" or a machine's `clientId` misread as a user's `sub`.
179
179
  *
180
180
  * The concrete reason this matters here: this identity server's
181
- * `client_credentials` grant mints access tokens with no end user and no
182
- * `roles` claim at all -- there is no subject for a roles claim to describe.
183
- * Before this type existed, `verifyAccessToken` returned one flat claims
184
- * object for both cases, so a caller checking `.sub` on a machine token
185
- * silently got `undefined` instead of a type error telling it this branch
186
- * has no subject.
181
+ * `client_credentials` grant mints access tokens with no end user -- there
182
+ * is no `sub` for this variant, ever. It CAN still carry a `roles` claim,
183
+ * resolved against the requesting client itself as the subject, whenever the
184
+ * client_credentials request's scope included `roles`; unlike `sub`, `roles`
185
+ * is not tied to there being an end user. Before this type existed,
186
+ * `verifyAccessToken` returned one flat claims object for both cases, so a
187
+ * caller checking `.sub` on a machine token silently got `undefined` instead
188
+ * of a type error telling it this branch has no subject.
187
189
  */
188
190
  type VerifiedAccessToken = {
189
191
  kind: 'user';
@@ -196,6 +198,11 @@ type VerifiedAccessToken = {
196
198
  } | {
197
199
  kind: 'machine';
198
200
  clientId: string;
201
+ /** Present only when the client_credentials request's scope included
202
+ * `roles` -- the server resolves the claim against the requesting
203
+ * client itself as the subject in that case. Pass to `permits()` from
204
+ * `@tdacorp/identity-authz` to decide a permission locally. */
205
+ roles?: RolesClaim;
199
206
  claims: Record<string, unknown>;
200
207
  };
201
208
 
@@ -178,12 +178,14 @@ declare function refreshTokens(options: RefreshTokensOptions): Promise<TokenRefr
178
178
  * read as "no roles" or a machine's `clientId` misread as a user's `sub`.
179
179
  *
180
180
  * The concrete reason this matters here: this identity server's
181
- * `client_credentials` grant mints access tokens with no end user and no
182
- * `roles` claim at all -- there is no subject for a roles claim to describe.
183
- * Before this type existed, `verifyAccessToken` returned one flat claims
184
- * object for both cases, so a caller checking `.sub` on a machine token
185
- * silently got `undefined` instead of a type error telling it this branch
186
- * has no subject.
181
+ * `client_credentials` grant mints access tokens with no end user -- there
182
+ * is no `sub` for this variant, ever. It CAN still carry a `roles` claim,
183
+ * resolved against the requesting client itself as the subject, whenever the
184
+ * client_credentials request's scope included `roles`; unlike `sub`, `roles`
185
+ * is not tied to there being an end user. Before this type existed,
186
+ * `verifyAccessToken` returned one flat claims object for both cases, so a
187
+ * caller checking `.sub` on a machine token silently got `undefined` instead
188
+ * of a type error telling it this branch has no subject.
187
189
  */
188
190
  type VerifiedAccessToken = {
189
191
  kind: 'user';
@@ -196,6 +198,11 @@ type VerifiedAccessToken = {
196
198
  } | {
197
199
  kind: 'machine';
198
200
  clientId: string;
201
+ /** Present only when the client_credentials request's scope included
202
+ * `roles` -- the server resolves the claim against the requesting
203
+ * client itself as the subject in that case. Pass to `permits()` from
204
+ * `@tdacorp/identity-authz` to decide a permission locally. */
205
+ roles?: RolesClaim;
199
206
  claims: Record<string, unknown>;
200
207
  };
201
208
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdacorp/identity-client",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "OIDC relying-party client for TDACorp Identity: discovery, PKCE, token exchange, token verification and Next.js route helpers.",
5
5
  "license": "MIT",
6
6
  "bugs": {