Skip to content

Commit

Permalink
Merge remote-tracking branch 'refs/remotes/origin/feature/CWALL-199' …
Browse files Browse the repository at this point in the history
…into feature/CWALL-174_impl-draft13_with-199

# Conflicts:
#	packages/client/lib/CredentialOfferClient.ts
#	packages/client/lib/__tests__/IT.spec.ts
#	packages/client/lib/__tests__/IssuanceInitiation.spec.ts
  • Loading branch information
sksadjad committed May 23, 2024
2 parents 125cb81 + f6c4d31 commit 2b7951a
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 21 deletions.
20 changes: 20 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ client you can use to finish the pre-authorized code flows.
This initiates the client using a URI obtained from the Issuer using a link (URL) or QR code typically. We are also
already fetching the Server Metadata


Using openid-initiate-issuance scheme
```typescript
import { OpenID4VCIClient } from '@sphereon/oid4vci-client';

Expand All @@ -68,6 +70,24 @@ console.log(client.getCredentialEndpoint()); // https://issuer.research.identipr
console.log(client.getAccessTokenEndpoint()); // https://auth.research.identiproof.io/oauth2/token
```

Using https scheme
```typescript
import { OpenID4VCIClient } from '@sphereon/oid4vci-client';

// The client is initiated from a URI. This URI is provided by the Issuer, typically as a URL or QR code.
const client = await OpenID4VCIClient.fromURI({
uri: 'https://launchpad.vii.electron.mattrlabs.io?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21#key-1', // Our DID. You can defer this also to when the acquireCredential method is called
alg: Alg.ES256, // The signing Algorithm we will use. You can defer this also to when the acquireCredential method is called
clientId: 'test-clientId', // The clientId if the Authrozation Service requires it. If a clientId is needed you can defer this also to when the acquireAccessToken method is called
retrieveServerMetadata: true, // Already retrieve the server metadata. Can also be done afterwards by invoking a method yourself.
});

console.log(client.getIssuer()); // https://launchpad.vii.electron.mattrlabs.io
console.log(client.getCredentialEndpoint()); // https://launchpad.vii.electron.mattrlabs.io/credential
console.log(client.getAccessTokenEndpoint()); // https://launchpad.vii.electron.mattrlabs.io/oauth2/token
```

## Server metadata

The OID4VCI Server metadata contains information about token endpoints, credential endpoints, as well as additional
Expand Down
45 changes: 41 additions & 4 deletions packages/client/lib/__tests__/IT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,14 @@ describe('OID4VCI-Client should', () => {
};
const mockedVC =
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA';
const INITIATE_QR_V1_0_08 =
const INITIATE_QR =
'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true';
const OFFER_QR_V1_0_08 =
'openid-credential-offer://credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D';
'openid-credential-offer://?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D';
const HTTPS_INITIATE_QR = 'https://issuer.research.identiproof.io?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true'
const HTTPS_OFFER_QR_AUTHORIZATION_CODE =
'https://issuer.research.identiproof.io?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D';
const HTTPS_OFFER_QR_PRE_AUTHORIZED = 'https://issuer.research.identiproof.io?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D'

const INITIATE_QR_V1_0_13 =
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://issuer.research.identiproof.io%22,%22credential_configuration_ids%22:%5B%22OpenBadgeCredentialUrl%22%5D,%22grants%22:%7B%22urn:ietf:params:oauth:grant-type:pre-authorized_code%22:%7B%22pre-authorized_code%22:%22oaKazRN8I0IbtZ0C7JuMn5%22,%22tx_code%22:%7B%22input_mode%22:%22text%22,%22length%22:22,%22description%22:%22Please%20enter%20the%20serial%20number%20of%20your%20physical%20drivers%20license%22%7D%7D%7D%7D';
Expand Down Expand Up @@ -91,7 +95,7 @@ describe('OID4VCI-Client should', () => {
await assertionOfsucceedWithAFullFlowWithClient(client);
});

test.skip('succeed with a full flow wit the client using OpenID4VCI version 11', async () => {
it('succeed with a full flow with the client using OpenID4VCI version 11 and deeplink', async () => {
succeedWithAFullFlowWithClientSetup();
const client = await OpenID4VCIClientV1_0_11.fromURI({
uri: OFFER_QR_V1_0_08,
Expand All @@ -102,14 +106,47 @@ describe('OID4VCI-Client should', () => {
await assertionOfsucceedWithAFullFlowWithClient(client);
});

it('succeed with a full flow with the client using OpenID4VCI draft < 9 and https', async () => {
succeedWithAFullFlowWithClientSetup()
const client = await OpenID4VCIClient.fromURI({
uri: HTTPS_INITIATE_QR,
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
alg: Alg.ES256,
clientId: 'test-clientId'
})
await assertionOfsucceedWithAFullFlowWithClient(client);
})

it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and authorization_code flow', async () => {
succeedWithAFullFlowWithClientSetup()
const client = await OpenID4VCIClient.fromURI({
uri: HTTPS_OFFER_QR_AUTHORIZATION_CODE,
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
alg: Alg.ES256,
clientId: 'test-clientId'
})
await assertionOfsucceedWithAFullFlowWithClient(client);
})

it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and preauthorized_code flow', async () => {
succeedWithAFullFlowWithClientSetup()
const client = await OpenID4VCIClient.fromURI({
uri: HTTPS_OFFER_QR_PRE_AUTHORIZED,
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
alg: Alg.ES256,
clientId: 'test-clientId'
})
await assertionOfsucceedWithAFullFlowWithClient(client);
})

async function assertionOfsucceedWithAFullFlowWithClient(client: OpenID4VCIClientV1_0_11) {
expect(client.credentialOffer).toBeDefined();
expect(client.endpointMetadata).toBeDefined();
expect(client.getIssuer()).toEqual('https://issuer.research.identiproof.io');
expect(client.getCredentialEndpoint()).toEqual('https://issuer.research.identiproof.io/credential');
expect(client.getAccessTokenEndpoint()).toEqual('https://auth.research.identiproof.io/oauth2/token');

const accessToken = await client.acquireAccessToken({ pin: '1234' });
const accessToken = await client.acquireAccessToken({ pin: '1234', code: 'ABCD' });
expect(accessToken).toEqual(mockedAccessTokenResponse);

const credentialResponse = await client.acquireCredentials({
Expand Down
63 changes: 55 additions & 8 deletions packages/client/lib/__tests__/IssuanceInitiation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
import { OpenId4VCIVersion } from '@sphereon/oid4vci-common';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import nock from 'nock';

import { CredentialOfferClient } from '../CredentialOfferClient';

import { INITIATION_TEST, INITIATION_TEST_HTTPS_URI, INITIATION_TEST_URI } from './MetadataMocks';

describe('Issuance Initiation', () => {
it('Should return Issuance Initiation Request with base URL from https URI', async () => {
expect(await CredentialOfferClient.fromURI(INITIATION_TEST_HTTPS_URI)).toEqual({
baseUrl: 'https://server.example.com',
credential_offer: {
credential_issuer: 'https://server.example.com',
credentials: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'],
grants: {
authorization_code: {
issuer_state: 'eyJhbGciOiJSU0Et...FYUaBy',
},
},
},
issuerState: 'eyJhbGciOiJSU0Et...FYUaBy',
original_credential_offer: {
credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'],
issuer: 'https://server.example.com',
op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
},
scheme: 'https',
supportedFlows: ['Authorization Code Flow'],
userPinRequired: false,
version: 1008,
});
});

it('Should return Issuance Initiation Request with base URL from openid-initiate-issuance URI', async () => {
expect(await CredentialOfferClient.fromURI(INITIATION_TEST_URI)).toEqual(INITIATION_TEST);
});

it('Should return Issuance Initiation URI from request', async () => {
expect(CredentialOfferClient.toURI(INITIATION_TEST)).toEqual(
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Foidc%2F%22%2C%22credential_configuration_ids%22%3A%5B%22OpenBadgeCredential%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE%22%2C%22tx_code%22%3A%7B%22description%22%3A%22Pleaseprovide%20the%20one-time%20code%20that%20was%20sent%20via%20e-mail%22%2C%22input_mode%22%3A%22numeric%22%2C%22length%22%3A4%7D%7D%7D%7D',
);
expect(CredentialOfferClient.toURI(INITIATION_TEST)).toEqual(INITIATION_TEST_URI);
});

it('Should return URI from Issuance Initiation Request', async () => {
const issuanceInitiationClient = await CredentialOfferClient.fromURI(INITIATION_TEST_HTTPS_URI);
expect(CredentialOfferClient.toURI(issuanceInitiationClient)).toEqual(INITIATION_TEST_HTTPS_URI);
});

it('Should throw error on invalid URI', async () => {
Expand All @@ -25,15 +50,37 @@ describe('Issuance Initiation', () => {

it('Should return Credential Offer', async () => {
const client = await CredentialOfferClient.fromURI(
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credential_configuration_ids%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
);
expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_13);
expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11);
expect(client.baseUrl).toEqual('openid-credential-offer://');
expect(client.scheme).toEqual('openid-credential-offer');
expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io');
expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X');
});

it('Should take an https url as input and return a Credential Offer', async () => {
const client = await CredentialOfferClient.fromURI(
'https://launchpad.vii.electron.mattrlabs.io?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
);
expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11);
expect(client.baseUrl).toEqual('https://launchpad.vii.electron.mattrlabs.io');
expect(client.scheme).toEqual('https');
expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io');
expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X');
})

it('Should take an http url as input and return a Credential Offer', async () => {
const client = await CredentialOfferClient.fromURI(
'http://launchpad.vii.electron.mattrlabs.io?credential_offer=%7B%22credential_issuer%22%3A%22http%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
);
expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11);
expect(client.baseUrl).toEqual('http://launchpad.vii.electron.mattrlabs.io');
expect(client.scheme).toEqual('http');
expect(client.credential_offer.credential_issuer).toEqual('http://launchpad.vii.electron.mattrlabs.io');
expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X');
})

it('Should return credenco Credential Offer', async () => {
nock('https://mijnkvk.acc.credenco.com')
.get('/openid4vc/credentialOffer?id=32fc4ebf-9e31-4149-9877-e3c0b602d559')
Expand Down
53 changes: 44 additions & 9 deletions packages/common/lib/functions/Encoding.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { BAD_PARAMS, DecodeURIAsJsonOpts, EncodeJsonAsURIOpts, JsonURIMode, OpenId4VCIVersion, SearchValue } from '../types';
import {
BAD_PARAMS,
CredentialOfferV1_0_11,
DecodeURIAsJsonOpts,
EncodeJsonAsURIOpts,
JsonURIMode,
OpenId4VCIVersion,
SearchValue
} from '../types'

/**
* @function encodeJsonAsURI encodes a Json object into a URI
* @param json object
* @param opts:
* @type {(json: {[s:string]: never} | ArrayLike<never> | string | object, opts?: EncodeJsonAsURIOpts)} encodes a Json object into a URI
* @param { {[s:string]: never} | ArrayLike<never> | string | object } json
* @param {EncodeJsonAsURIOpts} [opts] Option to encode json as uri
* - urlTypeProperties: a list of properties of which the value is a URL
* - arrayTypeProperties: a list of properties which are an array
*/
Expand Down Expand Up @@ -81,20 +89,47 @@ export function convertJsonToURI(
}

/**
* @function decodeUriAsJson decodes a URI into a Json object
* @param uri string
* @param opts:
* @type {(uri: string, opts?: DecodeURIAsJsonOpts): unknown} convertURIToJsonObject converts an URI into a Json object decoding its properties
* @param {string} uri
* @param {DecodeURIAsJsonOpts} [opts]
* - requiredProperties: the required properties
* - arrayTypeProperties: properties that can show up more that once
* @returns JSON object
*/
export function convertURIToJsonObject(uri: string, opts?: DecodeURIAsJsonOpts): unknown {
if (!uri || (opts?.requiredProperties && !opts.requiredProperties?.every((p) => uri.includes(p)))) {
throw new Error(BAD_PARAMS);
}

if (opts?.arrayTypeProperties && opts.arrayTypeProperties.includes('credential_offer_uri')) {
return encodedCredentialOfferUri2Json({ uri })
}

if (opts?.arrayTypeProperties && opts.arrayTypeProperties.includes('credential_offer')) {
return encodedCredentialOffer2Json({ uri })
}

const uriComponents = getURIComponentsAsArray(uri, opts?.arrayTypeProperties);
return decodeJsonProperties(uriComponents);
}

function encodedCredentialOfferUri2Json(args: { uri: string }): Pick<CredentialOfferV1_0_11, 'credential_offer_uri'> {
const { uri } = args
const value = uri.includes('?') ? uri.split('?')[1].split('=') : uri
return {
credential_offer_uri: value[1]
}
}

function encodedCredentialOffer2Json(args: { uri: string }): Pick<CredentialOfferV1_0_11, 'credential_offer'> {
const { uri } = args
const decodedUri = decodeURIComponent(uri)
const value = decodedUri.includes('?') ? decodedUri.split('?')[1].split('=') : decodedUri
return {
credential_offer: JSON.parse(value[1])
}
}

function decodeJsonProperties(parts: string[] | string[][]): unknown {
const json: { [s: string]: unknown } | ArrayLike<unknown> = {};
for (const key in parts) {
Expand Down Expand Up @@ -128,8 +163,8 @@ function decodeJsonProperties(parts: string[] | string[][]): unknown {

/**
* @function get URI Components as Array
* @param uri string
* @param arrayTypes array of string containing array like keys
* @param {string} uri uri
* @param {string[]} [arrayTypes] array of string containing array like keys
*/
function getURIComponentsAsArray(uri: string, arrayTypes?: string[]): string[] | string[][] {
const parts = uri.includes('?') ? uri.split('?')[1] : uri.includes('://') ? uri.split('://')[1] : uri;
Expand Down
Loading

0 comments on commit 2b7951a

Please sign in to comment.