Skip to content

Commit

Permalink
Merge pull request #99 from commercelayer/get-api-base-endpoint
Browse files Browse the repository at this point in the history
Add `getProvisioningApiBaseEndpoint` helper method
  • Loading branch information
marcomontalbano authored Nov 5, 2024
2 parents b019a2d + d312a4a commit 3217dc7
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 6 deletions.
67 changes: 67 additions & 0 deletions packages/js-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ It works everywhere. On your browser, server, or at the edge.
- [Decode an access token](#decode-an-access-token)
- [Verify an access token](#verify-an-access-token)
- [Get Core API base endpoint](#get-core-api-base-endpoint)
- [Get Provisioning API base endpoint](#get-provisioning-api-base-endpoint)
- [Contributors guide](#contributors-guide)
- [Need help?](#need-help)
- [License](#license)
Expand Down Expand Up @@ -69,6 +70,60 @@ To get an access token, you need to execute an [OAuth 2.0](https://oauth.net/2/)

Check our [documentation](https://docs.commercelayer.io/developers/authentication) for further information on each single authorization flow.

```mermaid
flowchart TB
%% Default style for nodes
classDef node stroke-width:2px;
%% Main nodes
auth(<b>Auth API</b><br/><br/>https://<b>auth</b>.commercelayer.io)
dashboard("dashboard")
user("user")
sales_channel("sales_channel")
integration("integration")
webapp("webapp")
provisioningAPI(<b>Provisioning API</b><br/><br/>https://<b>provisioning</b>.commercelayer.io)
coreAPI(<b>Core API</b><br/><br/>https://&lt;<b>slug</b>&gt;.commercelayer.io)
metricsAPI(<b>Metrics API</b><br/><br/>https://&lt;<b>slug</b>&gt;.commercelayer.io/metrics)
comingSoon(<b>Metrics API</b><br/><br/>https://<b>metrics</b>.commercelayer.io)
%% Node styles
style dashboard fill:#FFE6CC,stroke:#D79B00,color:#000
style user fill:#F8CECC,stroke:#B85450,color:#000
style sales_channel fill:#D5E8D4,stroke:#82B366,color:#000
style integration fill:#DAE8FC,stroke:#6C8EBF,color:#000
style webapp fill:#E1D5E7,stroke:#9673A6,color:#000
%% Connections
auth --> dashboard
auth --> user
auth --> sales_channel
auth --> integration
auth --> webapp
dashboard --> provisioningAPI
user --> provisioningAPI
user -- coming soon --> comingSoon
sales_channel --> coreAPI
integration --> coreAPI
integration --> metricsAPI
webapp --> coreAPI
webapp --> metricsAPI
%% Arrow Styles
linkStyle default stroke-width:2px;
linkStyle 5 stroke:#D79B00
linkStyle 6 stroke:#B85450
linkStyle 7 stroke:#B85450,stroke-dasharray: 5 5;
linkStyle 8 stroke:#82B366
linkStyle 9 stroke:#6C8EBF
linkStyle 10 stroke:#6C8EBF
linkStyle 11 stroke:#9673A6
linkStyle 12 stroke:#9673A6
```

## Use cases

Based on the authorization flow and application you want to use, you can get your access token in a few simple steps. These are the most common use cases:
Expand Down Expand Up @@ -339,6 +394,18 @@ getCoreApiBaseEndpoint('a-valid-access-token') //= "https://yourdomain.commercel

The method requires a valid access token with an `organization` in the payload. When the organization is not set (e.g., provisioning token), it throws an `InvalidTokenError` exception.

### Get Provisioning API base endpoint

It returns the [Provisioning API base endpoint](https://docs.commercelayer.io/provisioning/getting-started/api-specification#base-endpoint) given a valid access token.

```ts
import { getProvisioningApiBaseEndpoint } from '@commercelayer/js-auth'

getProvisioningApiBaseEndpoint('a-valid-access-token') //= "https://provisioning.commercelayer.io"
```

The method requires a valid access token (the token can be used with Provisioning API). When the token is not valid (e.g., core api token), it throws an `InvalidTokenError` exception.

---

## Contributors guide
Expand Down
9 changes: 9 additions & 0 deletions packages/js-auth/src/getCoreApiBaseEndpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ describe('getCoreApiBaseEndpoint', () => {
)
})

it('should return `null` when the access token does not have an "organization" and shouldThrow option is set to `false`.', () => {
const accessToken =
'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJ1c2VyIjp7ImlkIjoiZ2Jsb3dTeVZlcSJ9LCJhcHBsaWNhdGlvbiI6eyJpZCI6Im5HVnFhaWxWeU4iLCJraW5kIjoiZGFzaGJvYXJkIiwicHVibGljIjpmYWxzZX0sInNjb3BlIjoicHJvdmlzaW9uaW5nLWFwaSBtZXRyaWNzLWFwaSIsImV4cCI6MTcxMDk3NjY5NSwidGVzdCI6ZmFsc2UsInJhbmQiOjAuNzY1MjgwMDc2MDY1MjMwNywiaWF0IjoxNzEwOTY5NDk1LCJpc3MiOiJodHRwczovL2NvbW1lcmNlbGF5ZXIuaW8ifQ.YYk1PRFa8zcAlus8uaDFcJF7FRBtXYz-h--OYyuxJ0pc_qG0jdZ7lNgKxZC0Xnb4f9QmO3nHC4b4leGm6aAw8Yw4atZZaEDEkPrlG-ZegtdM4_X2Wbeul_Swkxo91PCIkYRMue0tl-zwl3dH_bS48IGOgOCbNWIcuHFvILaN_oXOHaeGfbVY5zXFfMK8P77TWZEoK0BYvmXIv2o_x_uYQZVcev7sSy1aX2zkikMFu54PIDl-II94ETT2g51QgNglDVh64qIFRvb24uPZo3woEBtd4ogupMRY5c3BvbxtfKHeASjT2NMxSkg-J55V7L4Wv5Q3Oh5p7ePz-95n7lG7uQ'

expect(getCoreApiBaseEndpoint(accessToken, { shouldThrow: false })).toBe(
null
)
})

it('should return the core api endpoint when access token contains an "organization".', () => {
expect(
getCoreApiBaseEndpoint(
Expand Down
59 changes: 55 additions & 4 deletions packages/js-auth/src/getCoreApiBaseEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,72 @@ import { jwtDecode } from './jwtDecode.js'
import { extractIssuer } from './utils/extractIssuer.js'

/**
* Derive the [Core API base endpoint](https://docs.commercelayer.io/core/api-specification#base-endpoint) given a valid access token.
* Derives the [Core API base endpoint](https://docs.commercelayer.io/core/api-specification#base-endpoint) given a valid access token.
*
* @example
* ```ts
* getCoreApiBaseEndpoint('eyJhbGciOiJS...') //= "https://yourdomain.commercelayer.io"
* ```
*
* The method requires a valid access token with an `organization` in the payload.
* When the organization is not set (e.g., provisioning token), it throws an `InvalidTokenError` exception.
*
* @param accessToken - The access token to decode.
* @param options - An options object to configure behavior.
* @returns The core API base endpoint as a string, or `null` if the token is invalid and `shouldThrow` is `false`.
* @throws InvalidTokenError - If the token is invalid and `shouldThrow` is true.
*/
export function getCoreApiBaseEndpoint(
accessToken: string,
options?: {
/**
* Whether to throw an error if the token is invalid.
* @default true
*/
shouldThrow?: true
}
): string

/**
* Derives the [Core API base endpoint](https://docs.commercelayer.io/core/api-specification#base-endpoint) given a valid access token.
*
* @example
* ```ts
* getCoreApiBaseEndpoint('eyJhbGciOiJS...') //= "https://yourdomain.commercelayer.io"
* ```
*
* The method requires a valid access token with an `organization` in the payload.
*
* @param accessToken - The access token to decode.
* @param options - An options object to configure behavior.
* @returns The core API base endpoint as a string, or `null` if the token is invalid and `shouldThrow` is `false`.
* @throws InvalidTokenError - If the token is invalid and `shouldThrow` is true.
*/
export function getCoreApiBaseEndpoint(accessToken: string): string {
export function getCoreApiBaseEndpoint(
accessToken: string,
options: {
/**
* Whether to throw an error if the token is invalid.
* @default true
*/
shouldThrow: false
}
): string | null

export function getCoreApiBaseEndpoint(
accessToken: string,
options: {
shouldThrow?: boolean
} = {}
): string | null {
const { shouldThrow = true } = options
const decodedJWT = jwtDecode(accessToken)

if (!('organization' in decodedJWT.payload)) {
throw new InvalidTokenError('Invalid token format')
if (shouldThrow) {
throw new InvalidTokenError('Invalid token format')
}

return null
}

return extractIssuer(decodedJWT).replace(
Expand Down
65 changes: 65 additions & 0 deletions packages/js-auth/src/getProvisioningApiBaseEndpoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { InvalidTokenError } from './errors/InvalidTokenError.js'
import { TokenError } from './errors/TokenError.js'
import { getProvisioningApiBaseEndpoint } from './getProvisioningApiBaseEndpoint.js'

describe('getProvisioningApiBaseEndpoint', () => {
it('should throw when the access token is not valid.', () => {
const accessToken =
'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJlbldveEZNT25wIiwic2x1ZyI6InRoZS1ibHVlLWJyYW5kLTMiLCJlbnRlcnByaXNlIjpmYWxzZSwicmVnaW9uIjoiZXUtd2VzdC0xIn0sImFwcGxpY2F0aW9uIjp7ImlkIjoibkdWcWFpRVlOQSIsImNsaWVudF9pZCI6IjF1bEs3ZXdkUUZyak90LVlxZ0RQeXBRRzFYSmVtUGdaUDFvd3NpSG9tRDQiLCJraW5kIjoic2FsZXNfY2hhbm5lbCIsInB1YmxpYyI6dHJ1ZX0sInNjb3BlIjoibWFya2V0OmFsbCIsImV4cCI6MTcyNTM5OTc3OCwidGVzdCI6dHJ1ZSwicmFuZCI6MC4yMTIzMDUzMTE1NDg3Njk5NywiaWF0IjoxNzI1MzkyNTc4LCJpc3MiOiJodHRwczovL2F1dGguY29tbWVyY2VsYXllci5jbyJ9.aOjfJXPDQ-jY__XlZuaAhpboDCGPuaszSMjgBlGJ7ubjWmmHGIvgOewNusTKRcrvhgWsUPeGeBii9SdkxP0tHMejXb0qS8RZ7lAxhvfHlekytdhizLrTUoqAwGckgX9FJTzRh0rA57cFXLSuXXUvB5aUtFZ3CfxZ_YIRWUNSvbAsknYmH1WAy54QjpvM9jFD3iXv3ak9Q9DnxC80N98lgWfgjEgoW67jQSLHQT0r-cgbT8dbVUDOkUp4PnUQkjLWHcoejRFMlORTlQTSGjW8pYsYJyKhUa8FJ9UWcRGp55xIJLmdqbVcdYo0R2ESz32ek-Uu3OTO-JAbABSC-oN5Dzo_BHxS7ct_TcZV9Qxr-fGdZXD0C5PljMqmAQMeSRoZjdSLSLLKq5dyUx714NT9urE_BVLTkZ-jU-15iyS3aWy0qgmMKu3NOBmF_j-P7ohNNjDsdMz2ofZ8r2E_0QBgpC1LwdWt4r0S4eabiTskM4r-wrQSbctZae0veIrKQ4C3k1Wr1wR9b6CvZXr-IqaYg1VilMiffmVhbYl60iWg3IK5REKJFJ2hNm18A78OiBO4A5KiL6lk3N_26C9dJa9HolUbpopiIVgZxXUXRw6EohnU0e5IBnQES-3q9T_37lNgAKlbHpqDKKEWjdwqb3GRMdxzMj0rxELyBRBbx-W8_9s'

expect(() => getProvisioningApiBaseEndpoint(accessToken)).toThrow(
InvalidTokenError
)
expect(() => getProvisioningApiBaseEndpoint(accessToken)).toThrow(
TokenError
)
expect(() => getProvisioningApiBaseEndpoint(accessToken)).toThrow(
'Invalid token format'
)
})

it('should return `null` when the access token does not have an "organization" and shouldThrow option is set to `false`.', () => {
const accessToken =
'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJlbldveEZNT25wIiwic2x1ZyI6InRoZS1ibHVlLWJyYW5kLTMiLCJlbnRlcnByaXNlIjpmYWxzZSwicmVnaW9uIjoiZXUtd2VzdC0xIn0sImFwcGxpY2F0aW9uIjp7ImlkIjoibkdWcWFpRVlOQSIsImNsaWVudF9pZCI6IjF1bEs3ZXdkUUZyak90LVlxZ0RQeXBRRzFYSmVtUGdaUDFvd3NpSG9tRDQiLCJraW5kIjoic2FsZXNfY2hhbm5lbCIsInB1YmxpYyI6dHJ1ZX0sInNjb3BlIjoibWFya2V0OmFsbCIsImV4cCI6MTcyNTM5OTc3OCwidGVzdCI6dHJ1ZSwicmFuZCI6MC4yMTIzMDUzMTE1NDg3Njk5NywiaWF0IjoxNzI1MzkyNTc4LCJpc3MiOiJodHRwczovL2F1dGguY29tbWVyY2VsYXllci5jbyJ9.aOjfJXPDQ-jY__XlZuaAhpboDCGPuaszSMjgBlGJ7ubjWmmHGIvgOewNusTKRcrvhgWsUPeGeBii9SdkxP0tHMejXb0qS8RZ7lAxhvfHlekytdhizLrTUoqAwGckgX9FJTzRh0rA57cFXLSuXXUvB5aUtFZ3CfxZ_YIRWUNSvbAsknYmH1WAy54QjpvM9jFD3iXv3ak9Q9DnxC80N98lgWfgjEgoW67jQSLHQT0r-cgbT8dbVUDOkUp4PnUQkjLWHcoejRFMlORTlQTSGjW8pYsYJyKhUa8FJ9UWcRGp55xIJLmdqbVcdYo0R2ESz32ek-Uu3OTO-JAbABSC-oN5Dzo_BHxS7ct_TcZV9Qxr-fGdZXD0C5PljMqmAQMeSRoZjdSLSLLKq5dyUx714NT9urE_BVLTkZ-jU-15iyS3aWy0qgmMKu3NOBmF_j-P7ohNNjDsdMz2ofZ8r2E_0QBgpC1LwdWt4r0S4eabiTskM4r-wrQSbctZae0veIrKQ4C3k1Wr1wR9b6CvZXr-IqaYg1VilMiffmVhbYl60iWg3IK5REKJFJ2hNm18A78OiBO4A5KiL6lk3N_26C9dJa9HolUbpopiIVgZxXUXRw6EohnU0e5IBnQES-3q9T_37lNgAKlbHpqDKKEWjdwqb3GRMdxzMj0rxELyBRBbx-W8_9s'

expect(
getProvisioningApiBaseEndpoint(accessToken, { shouldThrow: false })
).toBe(null)
})

it('should return the provisioning api endpoint when access token contains "provisioning-api" scope.', () => {
const accessToken =
'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJ1c2VyIjp7ImlkIjoiZ2Jsb3dTeVZlcSJ9LCJhcHBsaWNhdGlvbiI6eyJpZCI6Im5HVnFhaWxWeU4iLCJraW5kIjoidXNlciIsInB1YmxpYyI6ZmFsc2V9LCJzY29wZSI6InByb3Zpc2lvbmluZy1hcGkiLCJleHAiOjE3MTA5NTA0MzAsInRlc3QiOmZhbHNlLCJyYW5kIjowLjY2OTAzODQ1MzY1NjE5MzMsImlhdCI6MTcxMDk0MzIzMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmNvbW1lcmNlbGF5ZXIuaW8ifQ.VdTMLw3DD627vI6Xiulo-bKc-yRsAJJ2yNMWC4idbZzzucnKhM0743BzmOrHiMie-z6BkIYLkosKDtrCl08qNFqezQBUAngylyNpwkyh_xg10_oZnmujrf7dCyhWLlk6mw0yOFwGiI9-7D47xnqjB55zSqR5nBqxAD_2ldWM2I29PYBcaTQIk3N6WZWXYI11obtTUoV-B-VVjUTb9Kf9o39TVyh0M4KGmDvI04WCHZd1VNmxskg9w72uAK_0tUc9vQy6xpdRAqeEEd3g71D_y9NEx05OBuWpFEredVGaHjqw6c2WNGfsJxMiDqmhmAqwlOc7ur2zCskWRiYpp-RyXrdTozKKxYngLpMA8lTnT21nes6UUOivtISutHGic2OFFQXdSf-tj4BiQ6YuePoURN48ZJ1zxnDKhg93NluWQX5Ny8n09L22uDDRCDtV8B4tEJ0Z_mAT0UhhQyDWYOTE5DlB67PpBS5M4CIq7CdB8zKawgMputHAI0YmlVIQ9R7BS8EPyuaVRRb0STmg1h0CcJ6BeLiBaFacTd17hMvQPhxuNi6QRSxbzzJiz104e71XPa7eTMJ0BCYjBvvV5pgNGRNlrbQolIm-yDWs7GExzXFIhzw5jbh8i316eZLo4FTprL4UGosnR-3dkTtib0HmHQoIdosjx9yaj8UJhE9--jA'

expect(getProvisioningApiBaseEndpoint(accessToken)).toEqual(
'https://provisioning.commercelayer.io'
)
})

it('should return the provisioning api endpoint when access token contains "provisioning-api" scope (stg env).', () => {
const accessToken =
'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJ1c2VyIjp7ImlkIjoiZ2Jsb3dTeVZlcSJ9LCJhcHBsaWNhdGlvbiI6eyJpZCI6Im5HVnFhaWxWeU4iLCJraW5kIjoidXNlciIsInB1YmxpYyI6ZmFsc2V9LCJzY29wZSI6InByb3Zpc2lvbmluZy1hcGkiLCJleHAiOjE3MTA5NTA0MzAsInRlc3QiOmZhbHNlLCJyYW5kIjowLjY2OTAzODQ1MzY1NjE5MzMsImlhdCI6MTcxMDk0MzIzMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmNvbW1lcmNlbGF5ZXIuY28ifQ.EEOTNia5Er_ObuwC91AxXC1y8ORnQPA4d4i4girU_u1VEyzXdVzdgZXFY0rx993TR4m7b7AOD-O3XJHpQPNENBIDkA6nAiAA0yltsslXzgWoJGzk3xnMCS8jrdOjWmgHXOeMtkRjPiZxdPC_jv705bihqmmjDIg-oT9Ez_p7rlssfE2nkc7-YzOcFRKlFlZqIehWuNNykQ9_fTjYV_L5eaApsRYEXvPIiC4UAWOAOA0liIjFBwS1dMAK3xpnhXjBZV1yFeqqiyV7MXdyRhBernd-Htt72Z3_S9It-ke8vmUyFxKmr9H4OFRn7PNVS_cEQvBk5-8pondWrHl5RlNDMGhXWIzsvAKrjYtUEeLogDphjO9x7oaaC770FkiTlbqV54Mt1IslgoV_2J9tjF9aAln4OoenUxBGFVVcrVPwfoJGuQFUdQ8EkUWf7z6ecZFhtBopSN_-zf9dOeukb--SQ-85gZGX_CJ8EcrKcqbBso2OBc6qrTFHNbXHfInyuRK70iVLjZNlbPDKcKE9Z2CFswmoXz1BY75mWewSFYK04R1IcVhhmrJEZdmGWt3-p22lOp5BMj_EjYDrXDRyGT4LiBIG9W3ovLYun3L_Te5_fGlXk2WzEWoX3WtWKZF9EO9aYWsBFiPKNnUiR0c8-5gj3dUTExkix9p4NwZW_9QlK-E'

expect(getProvisioningApiBaseEndpoint(accessToken)).toEqual(
'https://provisioning.commercelayer.co'
)
})

it('should fallback to "commercelayer.io" when "iss" is expressed in an old format.', () => {
const accessToken =
'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJ1c2VyIjp7ImlkIjoiZ2Jsb3dTeVZlcSJ9LCJhcHBsaWNhdGlvbiI6eyJpZCI6Im5HVnFhaWxWeU4iLCJraW5kIjoidXNlciIsInB1YmxpYyI6ZmFsc2V9LCJzY29wZSI6InByb3Zpc2lvbmluZy1hcGkiLCJleHAiOjE3MTA5NTA0MzAsInRlc3QiOmZhbHNlLCJyYW5kIjowLjY2OTAzODQ1MzY1NjE5MzMsImlhdCI6MTcxMDk0MzIzMCwiaXNzIjoiaHR0cHM6Ly9jb21tZXJjZWxheWVyLmlvIn0.NB1PVDXU-CbatAkOKUyKDVo4e1YrracajM1JXUZCnDqGPyD2oMzCQC9ztqtOXrbvlV3FHIWm0yzd8yQvKokFjvPDDH9TvfuWgi_hFN-Dh_7IZBj0tUBfUmF694QfrUOoRfX5OX-jBkRk0IrlYUi2WleiilkSbTV9YdAiLNDWFA1MjeK7YS-QLzrrYL6RsUcII4qrDb7UZZOWiZiXTbZ1HFiSZacrZfu3Eu1BGKVUl8ZhhgYOJ1mCPlVmqn4OTnMfZby8M8Jvo3z7HDbC1-lCWMhoQ7o_PH-duA4DnaMyVrchw1S_3aSmVx6rWykvZ80d9Qz-8oSvqZwhkmnMFvUKvQ'

expect(getProvisioningApiBaseEndpoint(accessToken)).toEqual(
'https://provisioning.commercelayer.io'
)
})

it('should fallback to "commercelayer.io" when "iss" is not defined.', () => {
const accessToken =
'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYTRjYzYyOGQxZmNlM2ZiOTNhM2VlNTU4MjZlNDFjZmFmMThkYzJkZmYzYjA3MjIyNzQwMzgwZTkxOTlkNWQifQ.eyJ1c2VyIjp7ImlkIjoiZ2Jsb3dTeVZlcSJ9LCJhcHBsaWNhdGlvbiI6eyJpZCI6Im5HVnFhaWxWeU4iLCJraW5kIjoidXNlciIsInB1YmxpYyI6ZmFsc2V9LCJzY29wZSI6InByb3Zpc2lvbmluZy1hcGkiLCJleHAiOjE3MTA5NTA0MzAsInRlc3QiOmZhbHNlLCJyYW5kIjowLjY2OTAzODQ1MzY1NjE5MzMsImlhdCI6MTcxMDk0MzIzMH0.XAoaaiIW8QTMc8nwktP3VrAhhw4-zZQRWyGv08B3D91-ajCE71LFJe5lHKjL-0AhMdHwIWa6ZcmoFYQTl72Rp_AitFJeRv978b_6Ep1Qa9XFcf3qjt6DXH4qHfZn2lB7KtvaYANgVHHgGMrcnO8PkaMCY2qSBaD9OJjnZE8013uc1IyUXdvRk-HLTKmGWdjYSXFdl92FfSC-fL0BGva7snUkj0E9pptJxmxVJ3_Vm1M2SP8TSDHo1TTLybGXgZffCVrGRN-kCLkDhgSZlDNvyRyNj604VQJhV3q362z6UQ92LV_wKZVXAWLPXcY3WIp6rEkHOfSXWzxxQVWT82Mm0WLAdsLccc9EIWG3D8_ezpfKfr3HWzz42vny7GwkA9YAo-Cu9tmwmE0_DsFGJpnPUAUqMfC-gCuJMYX1u5aRrqo_j6VQkAhbBZXW77FpZ8FiHObkFYxAtdik2C_eg2gBCz0TBgO3vfJpji--4vLJt9-InRjb1EFy1xjQ4NdM6xaw8eEy1gK4OpyJj1z_jWc80Wm02uUIfeCTrrcrQdcP6wUG7fk60UoHEw8FkQLoRg79TgwE3TcvfVDROqwfmCE9Bu3K_lurWJHojaJV2kSU-xx6X15L7qdSbdvwsQbhhTDAZFscaMTzUiUw6gyYvm0-8wWG7C33AsrSgUWq_TYud3E'

expect(getProvisioningApiBaseEndpoint(accessToken)).toEqual(
'https://provisioning.commercelayer.io'
)
})
})
75 changes: 75 additions & 0 deletions packages/js-auth/src/getProvisioningApiBaseEndpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { InvalidTokenError } from './errors/InvalidTokenError.js'
import { jwtDecode } from './jwtDecode.js'
import { extractIssuer } from './utils/extractIssuer.js'

/**
* Returns the [Provisioning API base endpoint](https://docs.commercelayer.io/provisioning/getting-started/api-specification#base-endpoint) given a valid access token.
*
* @example
* ```ts
* getProvisioningApiBaseEndpoint('eyJhbGciOiJS...') //= "https://provisioning.commercelayer.io"
* ```
*
* The method requires a valid access token for Provisioning API.
*
* @param accessToken - The access token to decode.
* @param options - An options object to configure behavior.
* @returns The provisioning API base endpoint as a string, or `null` if the token is invalid and `shouldThrow` is `false`.
* @throws InvalidTokenError - If the token is invalid and `shouldThrow` is true.
*/
export function getProvisioningApiBaseEndpoint(
accessToken: string,
options?: {
/**
* Whether to throw an error if the token is invalid.
* @default true
*/
shouldThrow?: true
}
): string

/**
* Returns the [Provisioning API base endpoint](https://docs.commercelayer.io/provisioning/getting-started/api-specification#base-endpoint) given a valid access token.
*
* @example
* ```ts
* getProvisioningApiBaseEndpoint('eyJhbGciOiJS...') //= "https://provisioning.commercelayer.io"
* ```
*
* The method requires a valid access token for Provisioning API.
*
* @param accessToken - The access token to decode.
* @param options - An options object to configure behavior.
* @returns The provisioning API base endpoint as a string, or `null` if the token is invalid and `shouldThrow` is `false`.
* @throws InvalidTokenError - If the token is invalid and `shouldThrow` is true.
*/
export function getProvisioningApiBaseEndpoint(
accessToken: string,
options: {
/**
* Whether to throw an error if the token is invalid.
* @default true
*/
shouldThrow: false
}
): string | null

export function getProvisioningApiBaseEndpoint(
accessToken: string,
options: {
shouldThrow?: boolean
} = {}
): string | null {
const { shouldThrow = true } = options
const decodedJWT = jwtDecode(accessToken)

if (!decodedJWT?.payload?.scope?.includes('provisioning-api')) {
if (shouldThrow) {
throw new InvalidTokenError('Invalid token format')
}

return null
}

return extractIssuer(decodedJWT).replace('auth', 'provisioning')
}
1 change: 1 addition & 0 deletions packages/js-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type {
} from './types/index.js'

export { getCoreApiBaseEndpoint } from './getCoreApiBaseEndpoint.js'
export { getProvisioningApiBaseEndpoint } from './getProvisioningApiBaseEndpoint.js'

export { InvalidTokenError } from './errors/InvalidTokenError.js'
export { TokenError } from './errors/TokenError.js'
Expand Down
4 changes: 2 additions & 2 deletions packages/js-auth/src/jwtDecode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ export interface CommerceLayerJWT {
}

type Payload =
| JWTUser
| JWTDashboard
| JWTIntegration
| JWTUser
| JWTSalesChannel
| JWTIntegration
| JWTWebApp

interface JWTBase {
Expand Down

0 comments on commit 3217dc7

Please sign in to comment.