mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
Merge pull request #4849 from logto-io/gao-add-oidc-org-integration-tests
refactor(test): add integration tests for org token grant
This commit is contained in:
commit
73f348af89
7 changed files with 390 additions and 7 deletions
|
@ -3,7 +3,7 @@ const config = {
|
||||||
transform: {},
|
transform: {},
|
||||||
testPathIgnorePatterns: ['/node_modules/'],
|
testPathIgnorePatterns: ['/node_modules/'],
|
||||||
coverageProvider: 'v8',
|
coverageProvider: 'v8',
|
||||||
setupFilesAfterEnv: ['./jest.setup.js', './jest.setup.api.js'],
|
setupFilesAfterEnv: ['jest-matcher-specific-error', './jest.setup.js', './jest.setup.api.js'],
|
||||||
roots: ['./lib'],
|
roots: ['./lib'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^#src/(.*)\\.js(x)?$': '<rootDir>/lib/$1',
|
'^#src/(.*)\\.js(x)?$': '<rootDir>/lib/$1',
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
const config = {
|
const config = {
|
||||||
transform: {},
|
transform: {},
|
||||||
preset: 'jest-puppeteer',
|
preset: 'jest-puppeteer',
|
||||||
setupFilesAfterEnv: ['./jest.setup.js'],
|
setupFilesAfterEnv: ['jest-matcher-specific-error', './jest.setup.js'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^#src/(.*)\\.js(x)?$': '<rootDir>/lib/$1',
|
'^#src/(.*)\\.js(x)?$': '<rootDir>/lib/$1',
|
||||||
'^(chalk|inquirer)$': '<rootDir>/../shared/lib/esm/module-proxy.js',
|
'^(chalk|inquirer)$': '<rootDir>/../shared/lib/esm/module-proxy.js',
|
||||||
|
|
|
@ -38,7 +38,9 @@
|
||||||
"expect-puppeteer": "^9.0.0",
|
"expect-puppeteer": "^9.0.0",
|
||||||
"got": "^13.0.0",
|
"got": "^13.0.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
|
"jest-matcher-specific-error": "^1.0.0",
|
||||||
"jest-puppeteer": "^9.0.0",
|
"jest-puppeteer": "^9.0.0",
|
||||||
|
"jose": "^5.0.0",
|
||||||
"node-fetch": "^3.3.0",
|
"node-fetch": "^3.3.0",
|
||||||
"openapi-schema-validator": "^12.0.0",
|
"openapi-schema-validator": "^12.0.0",
|
||||||
"openapi-types": "^12.0.0",
|
"openapi-types": "^12.0.0",
|
||||||
|
|
|
@ -18,12 +18,11 @@ export const defaultConfig = {
|
||||||
};
|
};
|
||||||
export default class MockClient {
|
export default class MockClient {
|
||||||
public rawCookies: string[] = [];
|
public rawCookies: string[] = [];
|
||||||
|
|
||||||
protected readonly config: LogtoConfig;
|
protected readonly config: LogtoConfig;
|
||||||
|
protected readonly storage: MemoryStorage;
|
||||||
|
protected readonly logto: LogtoClient;
|
||||||
|
|
||||||
private navigateUrl?: string;
|
private navigateUrl?: string;
|
||||||
private readonly storage: MemoryStorage;
|
|
||||||
private readonly logto: LogtoClient;
|
|
||||||
private readonly api: Got;
|
private readonly api: Got;
|
||||||
|
|
||||||
constructor(config?: Partial<LogtoConfig>) {
|
constructor(config?: Partial<LogtoConfig>) {
|
||||||
|
|
|
@ -0,0 +1,377 @@
|
||||||
|
import assert from 'node:assert';
|
||||||
|
|
||||||
|
import { decodeAccessToken } from '@logto/js';
|
||||||
|
import { type LogtoConfig, Prompt } from '@logto/node';
|
||||||
|
import { GrantType, InteractionEvent, demoAppApplicationId } from '@logto/schemas';
|
||||||
|
import { isKeyInObject, removeUndefinedKeys } from '@silverhand/essentials';
|
||||||
|
import { HTTPError, got } from 'got';
|
||||||
|
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||||
|
|
||||||
|
import { putInteraction } from '#src/api/index.js';
|
||||||
|
import MockClient, { defaultConfig } from '#src/client/index.js';
|
||||||
|
import { demoAppRedirectUri } from '#src/constants.js';
|
||||||
|
import { processSession } from '#src/helpers/client.js';
|
||||||
|
import { OrganizationApiTest } from '#src/helpers/organization.js';
|
||||||
|
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||||
|
import { UserApiTest } from '#src/helpers/user.js';
|
||||||
|
import { generateUsername, generatePassword } from '#src/utils.js';
|
||||||
|
|
||||||
|
/** A helper class to simplify the test on grant errors. */
|
||||||
|
class GrantError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly statusCode: number,
|
||||||
|
public readonly body: unknown
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a grant error matcher that matches certain elements of the error response. */
|
||||||
|
const grantErrorContaining = (code: string, description: string, statusCode = 400) =>
|
||||||
|
new GrantError(
|
||||||
|
statusCode,
|
||||||
|
expect.objectContaining({
|
||||||
|
code,
|
||||||
|
error_description: description,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const accessDeniedError = grantErrorContaining(
|
||||||
|
'oidc.access_denied',
|
||||||
|
'user is not a member of the organization',
|
||||||
|
400
|
||||||
|
);
|
||||||
|
|
||||||
|
const issuer = defaultConfig.endpoint + '/oidc';
|
||||||
|
|
||||||
|
class MockOrganizationClient extends MockClient {
|
||||||
|
/** Perform the organization token grant. It may be replaced once our SDK supports it. */
|
||||||
|
async fetchOrganizationToken(organizationId?: string) {
|
||||||
|
const refreshToken = await this.getRefreshToken();
|
||||||
|
try {
|
||||||
|
const json = await got
|
||||||
|
.post(`${this.config.endpoint}/oidc/token`, {
|
||||||
|
form: removeUndefinedKeys({
|
||||||
|
grant_type: GrantType.OrganizationToken,
|
||||||
|
client_id: this.config.appId,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
organization_id: organizationId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.json();
|
||||||
|
if (isKeyInObject(json, 'refresh_token')) {
|
||||||
|
await this.storage.setItem('refreshToken', String(json.refresh_token));
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HTTPError) {
|
||||||
|
throw new GrantError(error.response.statusCode, JSON.parse(String(error.response.body)));
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An edited version that asserts the value is a record instead of an object */
|
||||||
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
value !== null && typeof value === 'object';
|
||||||
|
|
||||||
|
describe('OIDC organization token grant', () => {
|
||||||
|
const organizationApi = new OrganizationApiTest();
|
||||||
|
const userApi = new UserApiTest();
|
||||||
|
const username = generateUsername();
|
||||||
|
const password = generatePassword();
|
||||||
|
// eslint-disable-next-line @silverhand/fp/no-let
|
||||||
|
let userId = '';
|
||||||
|
|
||||||
|
const initClient = async (configOverrides?: Partial<LogtoConfig>) => {
|
||||||
|
const client = new MockOrganizationClient({
|
||||||
|
appId: demoAppApplicationId,
|
||||||
|
prompt: Prompt.Consent,
|
||||||
|
scopes: ['urn:logto:scope:organizations'],
|
||||||
|
resources: ['urn:logto:resource:organizations'],
|
||||||
|
...configOverrides,
|
||||||
|
});
|
||||||
|
await client.initSession(demoAppRedirectUri);
|
||||||
|
await client.successSend(putInteraction, {
|
||||||
|
event: InteractionEvent.SignIn,
|
||||||
|
identifier: { username, password },
|
||||||
|
});
|
||||||
|
const { redirectTo } = await client.submitInteraction();
|
||||||
|
await processSession(client, redirectTo);
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expect the response of the organization token grant. It validates the response json
|
||||||
|
* and the access token payload. Note it does not validate the signature of the access token.
|
||||||
|
*
|
||||||
|
* @param response The response of the organization token grant (json object).
|
||||||
|
* @param expectation The expected values of the response and the access token payload.
|
||||||
|
*/
|
||||||
|
const expectGrantResponse = (
|
||||||
|
response: unknown,
|
||||||
|
expectation: {
|
||||||
|
organizationId: string;
|
||||||
|
scopes: string[];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { scopes, organizationId } = expectation;
|
||||||
|
|
||||||
|
// Expect response
|
||||||
|
assert(isObject(response), new Error('response is not an object'));
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
access_token: expect.any(String),
|
||||||
|
expires_in: 3600,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
refresh_token: expect.any(String),
|
||||||
|
token_type: 'Bearer',
|
||||||
|
});
|
||||||
|
expect(response).not.toHaveProperty('id_token');
|
||||||
|
expect(String(response.scope).split(' ').filter(Boolean).slice().sort()).toStrictEqual(
|
||||||
|
scopes.slice().sort()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expect access token
|
||||||
|
const accessToken = decodeAccessToken(String(response.access_token));
|
||||||
|
|
||||||
|
expect(accessToken.jti).toEqual(expect.any(String));
|
||||||
|
expect(accessToken.aud).toBe(`urn:logto:organization:${organizationId}`);
|
||||||
|
expect(accessToken.sub).toBe(userId);
|
||||||
|
expect(accessToken.client_id).toBe(demoAppApplicationId);
|
||||||
|
expect(accessToken.iss).toBe(issuer);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the test environment with some pre-defined data. It covers almost all
|
||||||
|
* possible cases for a user. See the source code for details.
|
||||||
|
*/
|
||||||
|
const initOrganizations = async () => {
|
||||||
|
const [org1, org2, org3, org4] = await Promise.all([
|
||||||
|
organizationApi.create({ name: 'org1' }),
|
||||||
|
organizationApi.create({ name: 'org2' }),
|
||||||
|
organizationApi.create({ name: 'org3' }),
|
||||||
|
organizationApi.create({ name: 'org4' }),
|
||||||
|
]);
|
||||||
|
const { roleApi, scopeApi } = organizationApi;
|
||||||
|
|
||||||
|
await organizationApi.addUsers(org1.id, [userId]);
|
||||||
|
await organizationApi.addUsers(org2.id, [userId]);
|
||||||
|
await organizationApi.addUsers(org3.id, [userId]);
|
||||||
|
|
||||||
|
const [scope1, scope2, scope3] = await Promise.all([
|
||||||
|
scopeApi.create({ name: 'scope1' }),
|
||||||
|
scopeApi.create({ name: 'scope2' }),
|
||||||
|
scopeApi.create({ name: 'scope3' }),
|
||||||
|
]);
|
||||||
|
const [role1, role2, role3, role4] = await Promise.all([
|
||||||
|
roleApi.create({ name: 'role1' }),
|
||||||
|
roleApi.create({ name: 'role2' }),
|
||||||
|
roleApi.create({ name: 'role3' }),
|
||||||
|
roleApi.create({ name: 'role4' }),
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
roleApi.addScopes(role1.id, [scope1.id, scope2.id]),
|
||||||
|
roleApi.addScopes(role2.id, [scope2.id, scope3.id]),
|
||||||
|
roleApi.addScopes(role3.id, [scope1.id, scope3.id]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
organizationApi.addUserRoles(org1.id, userId, [role1.id]),
|
||||||
|
organizationApi.addUserRoles(org2.id, userId, [role1.id, role2.id]),
|
||||||
|
organizationApi.addUserRoles(org3.id, userId, [role4.id]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Object.freeze({
|
||||||
|
orgs: [org1, org2, org3, org4],
|
||||||
|
roles: [role1, role2, role3, role4],
|
||||||
|
} as const);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const { id } = await userApi.create({ username, password });
|
||||||
|
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||||
|
userId = id;
|
||||||
|
await enableAllPasswordSignInMethods();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await userApi.cleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanity checks', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
organizationApi.cleanUp(),
|
||||||
|
organizationApi.roleApi.cleanUp(),
|
||||||
|
organizationApi.scopeApi.cleanUp(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when organization id is not provided', async () => {
|
||||||
|
const client = await initClient();
|
||||||
|
await expect(client.fetchOrganizationToken()).rejects.toMatchError(
|
||||||
|
grantErrorContaining('oidc.invalid_request', "missing required parameter 'organization_id'")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when organizations scope is not requested', async () => {
|
||||||
|
const client = await initClient({ scopes: [] });
|
||||||
|
await expect(client.fetchOrganizationToken('1')).rejects.toMatchError(
|
||||||
|
grantErrorContaining('oidc.insufficient_scope', 'refresh token missing required scope', 403)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return access denied when organization id is invalid', async () => {
|
||||||
|
const client = await initClient();
|
||||||
|
await expect(client.fetchOrganizationToken('1')).rejects.toMatchError(accessDeniedError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return access denied when organization exists but user is not a member, then issue organization token after user is added to the organization', async () => {
|
||||||
|
const org = await organizationApi.create({ name: 'org' });
|
||||||
|
const client = await initClient();
|
||||||
|
|
||||||
|
// Not a member yet
|
||||||
|
await expect(client.fetchOrganizationToken(org.id)).rejects.toMatchError(accessDeniedError);
|
||||||
|
|
||||||
|
// Add user to the organization
|
||||||
|
await organizationApi.addUsers(org.id, [userId]);
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(org.id), {
|
||||||
|
organizationId: org.id,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove user from the organization
|
||||||
|
await organizationApi.deleteUser(org.id, userId);
|
||||||
|
await expect(client.fetchOrganizationToken(org.id)).rejects.toMatchError(accessDeniedError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not issue organization scopes when organization resource is not requested', async () => {
|
||||||
|
const { orgs } = await initOrganizations();
|
||||||
|
|
||||||
|
const client = await initClient({
|
||||||
|
scopes: ['urn:logto:scope:organizations', 'scope1', 'scope2'],
|
||||||
|
resources: [],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[0].id), {
|
||||||
|
organizationId: orgs[0].id,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[1].id), {
|
||||||
|
organizationId: orgs[1].id,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[2].id), {
|
||||||
|
organizationId: orgs[2].id,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should issue a signed JWT', async () => {
|
||||||
|
const org = await organizationApi.create({ name: 'org' });
|
||||||
|
const client = await initClient();
|
||||||
|
|
||||||
|
await organizationApi.addUsers(org.id, [userId]);
|
||||||
|
|
||||||
|
const response = await client.fetchOrganizationToken(org.id);
|
||||||
|
const rawToken = isObject(response) && String(response.access_token);
|
||||||
|
|
||||||
|
assert(typeof rawToken === 'string', new TypeError('access_token is not a string'));
|
||||||
|
|
||||||
|
await jwtVerify(
|
||||||
|
rawToken,
|
||||||
|
createRemoteJWKSet(new URL(defaultConfig.endpoint + '/oidc/jwks')),
|
||||||
|
{ issuer }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('permission checks', () => {
|
||||||
|
// eslint-disable-next-line @silverhand/fp/no-let
|
||||||
|
let context: Awaited<ReturnType<typeof initOrganizations>>;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||||
|
context = await initOrganizations();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
organizationApi.cleanUp(),
|
||||||
|
organizationApi.roleApi.cleanUp(),
|
||||||
|
organizationApi.scopeApi.cleanUp(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should issue organization token according to user's role in the organization", async () => {
|
||||||
|
const { orgs } = context;
|
||||||
|
const client = await initClient({
|
||||||
|
scopes: ['urn:logto:scope:organizations', 'scope1', 'scope2', 'scope3'],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[0].id), {
|
||||||
|
organizationId: orgs[0].id,
|
||||||
|
scopes: ['scope1', 'scope2'],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[1].id), {
|
||||||
|
organizationId: orgs[1].id,
|
||||||
|
scopes: ['scope1', 'scope2', 'scope3'],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[2].id), {
|
||||||
|
organizationId: orgs[2].id,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should down-scope according to the refresh token', async () => {
|
||||||
|
const { orgs } = context;
|
||||||
|
const client = await initClient({
|
||||||
|
scopes: ['urn:logto:scope:organizations', 'scope1', 'scope2'],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[0].id), {
|
||||||
|
organizationId: orgs[0].id,
|
||||||
|
scopes: ['scope1', 'scope2'],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[1].id), {
|
||||||
|
organizationId: orgs[1].id,
|
||||||
|
scopes: ['scope1', 'scope2'],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[2].id), {
|
||||||
|
organizationId: orgs[2].id,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to dynamically update scopes', async () => {
|
||||||
|
const { orgs, roles } = context;
|
||||||
|
const client = await initClient({
|
||||||
|
scopes: ['urn:logto:scope:organizations', 'scope1', 'scope2', 'scope3'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[0].id), {
|
||||||
|
organizationId: orgs[0].id,
|
||||||
|
scopes: ['scope1', 'scope2'],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[1].id), {
|
||||||
|
organizationId: orgs[1].id,
|
||||||
|
scopes: ['scope1', 'scope2', 'scope3'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update scopes
|
||||||
|
await Promise.all([
|
||||||
|
organizationApi.addUserRoles(orgs[0].id, userId, [roles[2].id]),
|
||||||
|
organizationApi.deleteUserRole(orgs[1].id, userId, roles[0].id),
|
||||||
|
organizationApi.deleteUserRole(orgs[1].id, userId, roles[1].id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[0].id), {
|
||||||
|
organizationId: orgs[0].id,
|
||||||
|
scopes: ['scope1', 'scope2', 'scope3'],
|
||||||
|
});
|
||||||
|
expectGrantResponse(await client.fetchOrganizationToken(orgs[1].id), {
|
||||||
|
organizationId: orgs[1].id,
|
||||||
|
scopes: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,7 +11,7 @@
|
||||||
"src/*"
|
"src/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"types": ["jest", "jest-puppeteer", "expect-puppeteer"],
|
"types": ["jest", "jest-matcher-specific-error", "jest-puppeteer", "expect-puppeteer"],
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3771,9 +3771,15 @@ importers:
|
||||||
jest:
|
jest:
|
||||||
specifier: ^29.5.0
|
specifier: ^29.5.0
|
||||||
version: 29.5.0(@types/node@18.11.18)(ts-node@10.9.1)
|
version: 29.5.0(@types/node@18.11.18)(ts-node@10.9.1)
|
||||||
|
jest-matcher-specific-error:
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
jest-puppeteer:
|
jest-puppeteer:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.0(puppeteer@21.0.0)
|
version: 9.0.0(puppeteer@21.0.0)
|
||||||
|
jose:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.0.1
|
||||||
node-fetch:
|
node-fetch:
|
||||||
specifier: ^3.3.0
|
specifier: ^3.3.0
|
||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
|
@ -15147,7 +15153,6 @@ packages:
|
||||||
|
|
||||||
/jose@5.0.1:
|
/jose@5.0.1:
|
||||||
resolution: {integrity: sha512-gRVzy7s3RRdGbXmcTdlOswJOjhwPLx1ijIgAqLY6ktzFpOJxxYn4l0fC2vHaHHi4YBX/5FOL3aY+6W0cvQgpug==}
|
resolution: {integrity: sha512-gRVzy7s3RRdGbXmcTdlOswJOjhwPLx1ijIgAqLY6ktzFpOJxxYn4l0fC2vHaHHi4YBX/5FOL3aY+6W0cvQgpug==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/js-base64@3.7.5:
|
/js-base64@3.7.5:
|
||||||
resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
|
resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
|
||||||
|
|
Loading…
Reference in a new issue