0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(core,phrases,schemas): upload user assets with azure storage (#3289)

This commit is contained in:
wangsijie 2023-03-07 14:00:12 +08:00 committed by GitHub
parent d98086ff9a
commit f25a9d343c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 503 additions and 33 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/core": minor
---
Add API for uploading user images to storage providers: Azure Storage.

View file

@ -25,6 +25,7 @@
"test:report": "codecov -F core"
},
"dependencies": {
"@azure/storage-blob": "^12.13.0",
"@koa/cors": "^4.0.0",
"@logto/cli": "workspace:*",
"@logto/connector-kit": "workspace:*",

View file

@ -4,6 +4,7 @@ import { findUp } from 'find-up';
import Koa from 'koa';
import { checkAlterationState } from './env-set/check-alteration-state.js';
import SystemContext from './tenants/SystemContext.js';
dotenv.config({ path: await findUp('.env', {}) });
@ -23,6 +24,7 @@ try {
checkRowLevelSecurity(EnvSet.queryClient),
checkAlterationState(await EnvSet.pool),
]);
await SystemContext.shared.loadStorageProviderConfig(await EnvSet.pool);
// Import last until init completed
const { default: initApp } = await import('./app/init.js');

View file

@ -44,7 +44,7 @@ describe('koaGuardMiddleware', () => {
});
// Use to bypass the context type assert
const defaultGuard = { body: undefined, query: undefined, params: undefined };
const defaultGuard = { body: undefined, query: undefined, params: undefined, files: undefined };
it('invalid body type should throw', async () => {
const ctx = {

View file

@ -10,27 +10,30 @@ import RequestError from '#src/errors/RequestError/index.js';
import ServerError from '#src/errors/ServerError/index.js';
import assertThat from '#src/utils/assert-that.js';
export type GuardConfig<QueryT, BodyT, ParametersT, ResponseT> = {
export type GuardConfig<QueryT, BodyT, ParametersT, ResponseT, FilesT> = {
query?: ZodType<QueryT>;
body?: ZodType<BodyT>;
params?: ZodType<ParametersT>;
response?: ZodType<ResponseT>;
status?: number | number[];
files?: ZodType<FilesT>;
};
export type GuardedRequest<QueryT, BodyT, ParametersT> = {
export type GuardedRequest<QueryT, BodyT, ParametersT, FilesT> = {
query: QueryT;
body: BodyT;
params: ParametersT;
files: FilesT;
};
export type WithGuardedRequestContext<
ContextT extends IRouterParamContext,
GuardQueryT,
GuardBodyT,
GuardParametersT
GuardParametersT,
GuardFilesT
> = ContextT & {
guard: GuardedRequest<GuardQueryT, GuardBodyT, GuardParametersT>;
guard: GuardedRequest<GuardQueryT, GuardBodyT, GuardParametersT, GuardFilesT>;
};
export type WithGuardConfig<
@ -38,9 +41,10 @@ export type WithGuardConfig<
GuardQueryT = unknown,
GuardBodyT = unknown,
GuardParametersT = unknown,
GuardResponseT = unknown
GuardResponseT = unknown,
GuardFilesT = undefined
> = Type & {
config: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT, GuardResponseT>;
config: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT, GuardResponseT, GuardFilesT>;
};
export const isGuardMiddleware = <Type extends IMiddleware>(
@ -49,7 +53,7 @@ export const isGuardMiddleware = <Type extends IMiddleware>(
function_.name === 'guardMiddleware' && has(function_, 'config');
const tryParse = <Output, Definition extends ZodTypeDef, Input>(
type: 'query' | 'body' | 'params',
type: 'query' | 'body' | 'params' | 'files',
guard: Optional<ZodType<Output, Definition, Input>>,
data: unknown
) => {
@ -66,21 +70,29 @@ export default function koaGuard<
GuardQueryT = undefined,
GuardBodyT = undefined,
GuardParametersT = undefined,
GuardResponseT = unknown
GuardResponseT = unknown,
GuardFilesT = undefined
>({
query,
body,
params,
response,
status,
}: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT, GuardResponseT>): MiddlewareType<
files,
}: GuardConfig<
GuardQueryT,
GuardBodyT,
GuardParametersT,
GuardResponseT,
GuardFilesT
>): MiddlewareType<
StateT,
WithGuardedRequestContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
WithGuardedRequestContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT, GuardFilesT>,
GuardResponseT
> {
const guard: MiddlewareType<
StateT,
WithGuardedRequestContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
WithGuardedRequestContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT, GuardFilesT>,
GuardResponseT
> = async (ctx, next) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, no-restricted-syntax
@ -88,7 +100,8 @@ export default function koaGuard<
query: tryParse('query', query, ctx.request.query),
body: tryParse('body', body, ctx.request.body),
params: tryParse('params', params, ctx.params),
} as GuardedRequest<GuardQueryT, GuardBodyT, GuardParametersT>; // Have to do this since it's too complicated for TS
files: tryParse('files', files, ctx.request.files),
} as GuardedRequest<GuardQueryT, GuardBodyT, GuardParametersT, GuardFilesT>; // Have to do this since it's too complicated for TS
return next();
};
@ -96,12 +109,14 @@ export default function koaGuard<
const guardMiddleware: WithGuardConfig<
MiddlewareType<
StateT,
WithGuardedRequestContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
WithGuardedRequestContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT, GuardFilesT>,
GuardResponseT
>
> = async function (ctx, next) {
if (body) {
return koaBody<StateT, ContextT>()(ctx, async () => guard(ctx, next));
if (body ?? files) {
return koaBody<StateT, ContextT>({ multipart: Boolean(files) })(ctx, async () =>
guard(ctx, next)
);
}
await guard(ctx, next);

View file

@ -26,6 +26,7 @@ import signInExperiencesRoutes from './sign-in-experience/index.js';
import statusRoutes from './status.js';
import swaggerRoutes from './swagger.js';
import type { AnonymousRouter, AuthedRouter } from './types.js';
import userAssetsRoutes from './user-assets.js';
import verificationCodeRoutes from './verification-code.js';
import wellKnownRoutes from './well-known.js';
@ -49,6 +50,7 @@ const createRouters = (tenant: TenantContext) => {
customPhraseRoutes(managementRouter, tenant);
hookRoutes(managementRouter, tenant);
verificationCodeRoutes(managementRouter, tenant);
userAssetsRoutes(managementRouter, tenant);
const anonymousRouter: AnonymousRouter = new Router();
phraseRoutes(anonymousRouter, tenant);

View file

@ -0,0 +1,80 @@
import { readFile } from 'fs/promises';
import { generateStandardId } from '@logto/core-kit';
import { format } from 'date-fns';
import { object } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import SystemContext from '#src/tenants/SystemContext.js';
import assertThat from '#src/utils/assert-that.js';
import {
allowUploadMimeTypes,
maxUploadFileSize,
uploadFileGuard,
} from '#src/utils/storage/consts.js';
import { buildUploadFile } from '#src/utils/storage/index.js';
import { getTenantId } from '#src/utils/tenant.js';
import type { AuthedRouter, RouterInitArgs } from './types.js';
export default function userAssetsRoutes<T extends AuthedRouter>(...[router]: RouterInitArgs<T>) {
router.get('/user-assets/service-status', async (ctx, next) => {
const { storageProviderConfig } = SystemContext.shared;
ctx.body = storageProviderConfig
? {
status: 'ready',
allowUploadMimeTypes,
maxUploadFileSize,
}
: {
status: 'not_configured',
};
return next();
});
router.post(
'/user-assets',
koaGuard({
files: object({
file: uploadFileGuard,
}),
}),
async (ctx, next) => {
const { file } = ctx.guard.files;
assertThat(file.size <= maxUploadFileSize, 'guard.file_size_exceeded');
assertThat(allowUploadMimeTypes.includes(file.mimetype), 'guard.mime_type_not_allowed');
const tenantId = getTenantId(ctx.URL);
assertThat(tenantId, 'guard.can_not_get_tenant_id');
const { storageProviderConfig } = SystemContext.shared;
assertThat(storageProviderConfig, 'storage.not_configured');
const userId = ctx.auth.id;
const uploadFile = buildUploadFile(storageProviderConfig);
const objectKey = `${tenantId}/${userId}/${format(
new Date(),
'yyyy/MM/dd'
)}/${generateStandardId(8)}/${file.originalFilename}`;
try {
const { url } = await uploadFile(await readFile(file.filepath), objectKey, {
contentType: file.mimetype,
publicUrl: storageProviderConfig.publicUrl,
});
ctx.body = {
url,
};
} catch {
throw new RequestError('storage.upload_error');
}
return next();
}
);
}

View file

@ -0,0 +1,33 @@
import type { StorageProviderData } from '@logto/schemas';
import { storageProviderDataGuard, StorageProviderKey, Systems } from '@logto/schemas';
import { convertToIdentifiers } from '@logto/shared';
import type { CommonQueryMethods } from 'slonik';
import { sql } from 'slonik';
const { table, fields } = convertToIdentifiers(Systems);
export default class SystemContext {
static shared = new SystemContext();
public storageProviderConfig: StorageProviderData | undefined;
async loadStorageProviderConfig(pool: CommonQueryMethods) {
const record = await pool.maybeOne<Record<string, unknown>>(sql`
select ${fields.value} from ${table}
where ${fields.key} = ${StorageProviderKey.StorageProvider}
`);
if (!record) {
return;
}
const result = storageProviderDataGuard.safeParse(record.value);
if (!result.success) {
console.error('Failed to parse storage provider config:', result.error);
return;
}
this.storageProviderConfig = result.data;
}
}

View file

@ -83,7 +83,14 @@ export default class Tenant implements TenantContext {
const provider = initOidc(envSet, queries, libraries);
app.use(mount('/oidc', provider.app));
const tenantContext: TenantContext = { id, provider, queries, libraries, modelRouters, envSet };
const tenantContext: TenantContext = {
id,
provider,
queries,
libraries,
modelRouters,
envSet,
};
// Mount APIs
app.use(mount('/api', initApis(tenantContext)));

View file

@ -0,0 +1,32 @@
import { BlobServiceClient } from '@azure/storage-blob';
import type { UploadFile } from './types.js';
const defaultPublicDomain = 'blob.core.windows.net';
export const buildAzureStorage = (connectionString: string, container: string) => {
const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
const containerClient = blobServiceClient.getContainerClient(container);
const uploadFile: UploadFile = async (
data: Buffer,
objectKey: string,
{ contentType, publicUrl } = {}
) => {
const blockBlobClient = containerClient.getBlockBlobClient(objectKey);
await blockBlobClient.uploadData(data, {
blobHTTPHeaders: contentType ? { blobContentType: contentType } : undefined,
});
if (publicUrl) {
return { url: `${publicUrl}/${objectKey}` };
}
return {
url: `https://${blobServiceClient.accountName}.${defaultPublicDomain}/${container}/${objectKey}`,
};
};
return { uploadFile };
};

View file

@ -0,0 +1,21 @@
import { number, object, string } from 'zod';
export const maxUploadFileSize = 8 * 1024 * 1024; // 8MB
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
export const allowUploadMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/vnd.microsoft.icon',
'image/svg+xml',
'image/tiff',
'image/webp',
'image/bmp',
];
export const uploadFileGuard = object({
filepath: string(),
mimetype: string(),
originalFilename: string(),
size: number(),
});

View file

@ -0,0 +1,14 @@
import type { StorageProviderData } from '@logto/schemas';
import { buildAzureStorage } from './azure-storage.js';
import type { UploadFile } from './types.js';
export const buildUploadFile = (config: StorageProviderData): UploadFile => {
if (config.provider === 'AzureStorage') {
const storage = buildAzureStorage(config.connectionString, config.container);
return storage.uploadFile;
}
throw new Error('provider not supported');
};

View file

@ -0,0 +1,10 @@
export type UploadFileOptions = {
contentType?: string;
publicUrl?: string;
};
export type UploadFile = (
data: Buffer,
objectKey: string,
options?: UploadFileOptions
) => Promise<{ url: string }>;

View file

@ -16,6 +16,9 @@ const errors = {
guard: {
invalid_input: 'Die Anfrage {{type}} ist ungültig.',
invalid_pagination: 'Die Paginierung der Anfrage ist ungültig.',
can_not_get_tenant_id: 'Unable to get tenant id from request.', // UNTRANSLATED
file_size_exceeded: 'File size exceeded.', // UNTRANSLATED
mime_type_not_allowed: 'Mime type is not allowed.', // UNTRANSLATED
},
oidc: {
aborted: 'Der Endnutzer hat die Interaktion abgebrochen.',
@ -189,6 +192,11 @@ const errors = {
name_exists: 'The scope name {{name}} is already in use', // UNTRANSLATED
name_with_space: 'The name of the scope cannot contain any spaces.', // UNTRANSLATED
},
storage: {
not_configured: 'Storage provider is not configured.', // UNTRANSLATED
missing_parameter: 'Missing parameter {{parameter}} for storage provider.', // UNTRANSLATED
upload_error: 'Failed to upload file to the storage provider.', // UNTRANSLATED
},
};
export default errors;

View file

@ -16,6 +16,9 @@ const errors = {
guard: {
invalid_input: 'The request {{type}} is invalid.',
invalid_pagination: 'The request pagination value is invalid.',
can_not_get_tenant_id: 'Unable to get tenant id from request.',
file_size_exceeded: 'File size exceeded.',
mime_type_not_allowed: 'Mime type is not allowed.',
},
oidc: {
aborted: 'The end-user aborted interaction.',
@ -188,6 +191,11 @@ const errors = {
name_exists: 'The scope name {{name}} is already in use',
name_with_space: 'The name of the scope cannot contain any spaces.',
},
storage: {
not_configured: 'Storage provider is not configured.',
missing_parameter: 'Missing parameter {{parameter}} for storage provider.',
upload_error: 'Failed to upload file to the storage provider.',
},
};
export default errors;

View file

@ -17,6 +17,9 @@ const errors = {
guard: {
invalid_input: "La requête {{type}} n'est pas valide.",
invalid_pagination: "La valeur de la pagination de la requête n'est pas valide.",
can_not_get_tenant_id: 'Unable to get tenant id from request.', // UNTRANSLATED
file_size_exceeded: 'File size exceeded.', // UNTRANSLATED
mime_type_not_allowed: 'Mime type is not allowed.', // UNTRANSLATED
},
oidc: {
aborted: "L'utilisateur a abandonné l'interaction.",
@ -195,6 +198,11 @@ const errors = {
name_exists: 'The scope name {{name}} is already in use', // UNTRANSLATED
name_with_space: 'The name of the scope cannot contain any spaces.', // UNTRANSLATED
},
storage: {
not_configured: 'Storage provider is not configured.', // UNTRANSLATED
missing_parameter: 'Missing parameter {{parameter}} for storage provider.', // UNTRANSLATED
upload_error: 'Failed to upload file to the storage provider.', // UNTRANSLATED
},
};
export default errors;

View file

@ -16,6 +16,9 @@ const errors = {
guard: {
invalid_input: '{{type}} 요청 타입은 유효하지 않아요.',
invalid_pagination: '요청의 Pagination 값이 유효하지 않아요.',
can_not_get_tenant_id: 'Unable to get tenant id from request.', // UNTRANSLATED
file_size_exceeded: 'File size exceeded.', // UNTRANSLATED
mime_type_not_allowed: 'Mime type is not allowed.', // UNTRANSLATED
},
oidc: {
aborted: 'End 사용자가 상호 작용을 중단했어요.',
@ -181,6 +184,11 @@ const errors = {
name_exists: '범위 이름 {{name}}이/가 이미 사용 중이에요.',
name_with_space: '범위 이름에 공백을 포함할 수 없어요.',
},
storage: {
not_configured: 'Storage provider is not configured.', // UNTRANSLATED
missing_parameter: 'Missing parameter {{parameter}} for storage provider.', // UNTRANSLATED
upload_error: 'Failed to upload file to the storage provider.', // UNTRANSLATED
},
};
export default errors;

View file

@ -16,6 +16,9 @@ const errors = {
guard: {
invalid_input: 'A solicitação {{type}} é inválida.',
invalid_pagination: 'O valor de paginação da solicitação é inválido.',
can_not_get_tenant_id: 'Unable to get tenant id from request.', // UNTRANSLATED
file_size_exceeded: 'File size exceeded.', // UNTRANSLATED
mime_type_not_allowed: 'Mime type is not allowed.', // UNTRANSLATED
},
oidc: {
aborted: 'A interação abortada pelo end-user',
@ -196,6 +199,11 @@ const errors = {
name_exists: 'The scope name {{name}} is already in use', // UNTRANSLATED
name_with_space: 'The name of the scope cannot contain any spaces.', // UNTRANSLATED
},
storage: {
not_configured: 'Storage provider is not configured.', // UNTRANSLATED
missing_parameter: 'Missing parameter {{parameter}} for storage provider.', // UNTRANSLATED
upload_error: 'Failed to upload file to the storage provider.', // UNTRANSLATED
},
};
export default errors;

View file

@ -15,6 +15,9 @@ const errors = {
guard: {
invalid_input: 'O pedido {{type}} é inválido.',
invalid_pagination: 'O valor de paginação enviado é inválido.',
can_not_get_tenant_id: 'Unable to get tenant id from request.', // UNTRANSLATED
file_size_exceeded: 'File size exceeded.', // UNTRANSLATED
mime_type_not_allowed: 'Mime type is not allowed.', // UNTRANSLATED
},
oidc: {
aborted: 'O utilizador final abortou a interação.',
@ -190,6 +193,11 @@ const errors = {
name_exists: 'The scope name {{name}} is already in use', // UNTRANSLATED
name_with_space: 'The name of the scope cannot contain any spaces.', // UNTRANSLATED
},
storage: {
not_configured: 'Storage provider is not configured.', // UNTRANSLATED
missing_parameter: 'Missing parameter {{parameter}} for storage provider.', // UNTRANSLATED
upload_error: 'Failed to upload file to the storage provider.', // UNTRANSLATED
},
};
export default errors;

View file

@ -16,6 +16,9 @@ const errors = {
guard: {
invalid_input: 'İstek {{type}} geçersiz.',
invalid_pagination: 'İstenen sayfalandırma değeri geçersiz.',
can_not_get_tenant_id: 'Unable to get tenant id from request.', // UNTRANSLATED
file_size_exceeded: 'File size exceeded.', // UNTRANSLATED
mime_type_not_allowed: 'Mime type is not allowed.', // UNTRANSLATED
},
oidc: {
aborted: 'Son kullanıcı etkileşimi iptal etti.',
@ -190,6 +193,11 @@ const errors = {
name_exists: 'The scope name {{name}} is already in use', // UNTRANSLATED
name_with_space: 'The name of the scope cannot contain any spaces.', // UNTRANSLATED
},
storage: {
not_configured: 'Storage provider is not configured.', // UNTRANSLATED
missing_parameter: 'Missing parameter {{parameter}} for storage provider.', // UNTRANSLATED
upload_error: 'Failed to upload file to the storage provider.', // UNTRANSLATED
},
};
export default errors;

View file

@ -15,6 +15,9 @@ const errors = {
guard: {
invalid_input: '请求中 {{type}} 无效',
invalid_pagination: '分页参数无效',
can_not_get_tenant_id: 'Unable to get tenant id from request.', // UNTRANSLATED
file_size_exceeded: 'File size exceeded.', // UNTRANSLATED
mime_type_not_allowed: 'Mime type is not allowed.', // UNTRANSLATED
},
oidc: {
aborted: '用户终止了交互。',
@ -170,6 +173,11 @@ const errors = {
name_exists: 'The scope name {{name}} is already in use', // UNTRANSLATED
name_with_space: 'The name of the scope cannot contain any spaces.', // UNTRANSLATED
},
storage: {
not_configured: 'Storage provider is not configured.', // UNTRANSLATED
missing_parameter: 'Missing parameter {{parameter}} for storage provider.', // UNTRANSLATED
upload_error: 'Failed to upload file to the storage provider.', // UNTRANSLATED
},
};
export default errors;

View file

@ -36,7 +36,6 @@ export type AdminConsoleData = z.infer<typeof adminConsoleDataGuard>;
export enum AdminConsoleConfigKey {
AdminConsole = 'adminConsole',
}
export type AdminConsoleConfigType = {
[AdminConsoleConfigKey.AdminConsole]: AdminConsoleData;
};

View file

@ -21,13 +21,59 @@ export const alterationStateGuard: Readonly<{
}),
});
// Summary
export type SystemKey = AlterationStateKey;
export type SystemType = AlterationStateType;
export type SystemGuard = typeof alterationStateGuard;
// Storage provider
export enum StorageProvider {
AzureStorage = 'AzureStorage',
S3Storage = 'S3Storage',
}
export const systemKeys: readonly SystemKey[] = Object.freeze(Object.values(AlterationStateKey));
const basicConfig = {
publicUrl: z.string().optional(),
};
export const storageProviderDataGuard = z.discriminatedUnion('provider', [
z.object({
provider: z.literal(StorageProvider.AzureStorage),
connectionString: z.string(),
container: z.string(),
...basicConfig,
}),
z.object({
provider: z.literal(StorageProvider.S3Storage),
endpoint: z.string(),
accessKeyId: z.string(),
accessSecretKey: z.string(),
...basicConfig,
}),
]);
export type StorageProviderData = z.infer<typeof storageProviderDataGuard>;
export enum StorageProviderKey {
StorageProvider = 'storageProvider',
}
export type StorageProviderType = {
[StorageProviderKey.StorageProvider]: StorageProviderData;
};
export const storageProviderGuard: Readonly<{
[key in StorageProviderKey]: ZodType<StorageProviderType[key]>;
}> = Object.freeze({
[StorageProviderKey.StorageProvider]: storageProviderDataGuard,
});
// Summary
export type SystemKey = AlterationStateKey | StorageProviderKey;
export type SystemType = AlterationStateType | StorageProviderType;
export type SystemGuard = typeof alterationStateGuard & typeof storageProviderGuard;
export const systemKeys: readonly SystemKey[] = Object.freeze([
...Object.values(AlterationStateKey),
...Object.values(StorageProviderKey),
]);
export const systemGuards: SystemGuard = Object.freeze({
...alterationStateGuard,
...storageProviderGuard,
});

159
pnpm-lock.yaml generated
View file

@ -313,6 +313,7 @@ importers:
packages/core:
specifiers:
'@azure/storage-blob': ^12.13.0
'@koa/cors': ^4.0.0
'@logto/cli': workspace:*
'@logto/connector-kit': workspace:*
@ -391,6 +392,7 @@ importers:
typescript: ^4.9.4
zod: ^3.20.2
dependencies:
'@azure/storage-blob': 12.13.0
'@koa/cors': 4.0.0
'@logto/cli': link:../cli
'@logto/connector-kit': link:../toolkit/connector-kit
@ -972,6 +974,98 @@ packages:
'@jridgewell/trace-mapping': 0.3.17
dev: true
/@azure/abort-controller/1.1.0:
resolution: {integrity: sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==}
engines: {node: '>=12.0.0'}
dependencies:
tslib: 2.4.1
dev: false
/@azure/core-auth/1.4.0:
resolution: {integrity: sha512-HFrcTgmuSuukRf/EdPmqBrc5l6Q5Uu+2TbuhaKbgaCpP2TfAeiNaQPAadxO+CYBRHGUzIDteMAjFspFLDLnKVQ==}
engines: {node: '>=12.0.0'}
dependencies:
'@azure/abort-controller': 1.1.0
tslib: 2.4.1
dev: false
/@azure/core-http/3.0.0:
resolution: {integrity: sha512-BxI2SlGFPPz6J1XyZNIVUf0QZLBKFX+ViFjKOkzqD18J1zOINIQ8JSBKKr+i+v8+MB6LacL6Nn/sP/TE13+s2Q==}
engines: {node: '>=14.0.0'}
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/core-auth': 1.4.0
'@azure/core-tracing': 1.0.0-preview.13
'@azure/core-util': 1.2.0
'@azure/logger': 1.0.4
'@types/node-fetch': 2.6.2
'@types/tunnel': 0.0.3
form-data: 4.0.0
node-fetch: 2.6.7
process: 0.11.10
tslib: 2.4.1
tunnel: 0.0.6
uuid: 8.3.2
xml2js: 0.4.23
transitivePeerDependencies:
- encoding
dev: false
/@azure/core-lro/2.5.1:
resolution: {integrity: sha512-JHQy/bA3NOz2WuzOi5zEk6n/TJdAropupxUT521JIJvW7EXV2YN2SFYZrf/2RHeD28QAClGdynYadZsbmP+nyQ==}
engines: {node: '>=14.0.0'}
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/logger': 1.0.4
tslib: 2.4.1
dev: false
/@azure/core-paging/1.5.0:
resolution: {integrity: sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==}
engines: {node: '>=14.0.0'}
dependencies:
tslib: 2.4.1
dev: false
/@azure/core-tracing/1.0.0-preview.13:
resolution: {integrity: sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==}
engines: {node: '>=12.0.0'}
dependencies:
'@opentelemetry/api': 1.4.0
tslib: 2.4.1
dev: false
/@azure/core-util/1.2.0:
resolution: {integrity: sha512-ffGIw+Qs8bNKNLxz5UPkz4/VBM/EZY07mPve1ZYFqYUdPwFqRj0RPk0U7LZMOfT7GCck9YjuT1Rfp1PApNl1ng==}
engines: {node: '>=14.0.0'}
dependencies:
'@azure/abort-controller': 1.1.0
tslib: 2.4.1
dev: false
/@azure/logger/1.0.4:
resolution: {integrity: sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==}
engines: {node: '>=14.0.0'}
dependencies:
tslib: 2.4.1
dev: false
/@azure/storage-blob/12.13.0:
resolution: {integrity: sha512-t3Q2lvBMJucgTjQcP5+hvEJMAsJSk0qmAnjDLie2td017IiduZbbC9BOcFfmwzR6y6cJdZOuewLCNFmEx9IrXA==}
engines: {node: '>=14.0.0'}
dependencies:
'@azure/abort-controller': 1.1.0
'@azure/core-http': 3.0.0
'@azure/core-lro': 2.5.1
'@azure/core-paging': 1.5.0
'@azure/core-tracing': 1.0.0-preview.13
'@azure/logger': 1.0.4
events: 3.3.0
tslib: 2.4.1
transitivePeerDependencies:
- encoding
dev: false
/@babel/code-frame/7.18.6:
resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
engines: {node: '>=6.9.0'}
@ -2624,6 +2718,11 @@ packages:
fastq: 1.13.0
dev: true
/@opentelemetry/api/1.4.0:
resolution: {integrity: sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g==}
engines: {node: '>=8.0.0'}
dev: false
/@parcel/bundler-default/2.8.3_@parcel+core@2.8.3:
resolution: {integrity: sha512-yJvRsNWWu5fVydsWk3O2L4yIy3UZiKWO2cPDukGOIWMgp/Vbpp+2Ct5IygVRtE22bnseW/E/oe0PV3d2IkEJGg==}
engines: {node: '>= 12.0.0', parcel: ^2.8.3}
@ -4241,6 +4340,13 @@ packages:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: true
/@types/node-fetch/2.6.2:
resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
dependencies:
'@types/node': 18.11.18
form-data: 3.0.1
dev: false
/@types/node/12.20.55:
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
dev: true
@ -4414,6 +4520,12 @@ packages:
resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==}
dev: true
/@types/tunnel/0.0.3:
resolution: {integrity: sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==}
dependencies:
'@types/node': 18.11.18
dev: false
/@types/unist/2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: true
@ -4881,7 +4993,6 @@ packages:
/asynckit/0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: true
/axe-core/4.4.3:
resolution: {integrity: sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==}
@ -5534,7 +5645,6 @@ packages:
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: true
/comma-separated-tokens/1.0.8:
resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==}
@ -6106,7 +6216,6 @@ packages:
/delayed-stream/1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: true
/delegates/1.0.0:
resolution: {integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=}
@ -6946,6 +7055,11 @@ packages:
/eventemitter3/4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
/events/3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
dev: false
/execa/5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@ -7264,6 +7378,15 @@ packages:
resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==}
engines: {node: '>= 14.17'}
/form-data/3.0.1:
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/form-data/4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
@ -7271,7 +7394,6 @@ packages:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: true
/format/0.2.2:
resolution: {integrity: sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=}
@ -11052,7 +11174,6 @@ packages:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: true
/node-fetch/3.3.0:
resolution: {integrity: sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==}
@ -12167,9 +12288,8 @@ packages:
dev: true
/process/0.11.10:
resolution: {integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=}
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
dev: true
/progress/2.0.3:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
@ -13087,6 +13207,10 @@ packages:
source-map-js: 1.0.2
dev: true
/sax/1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: false
/saxes/6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
@ -14137,7 +14261,6 @@ packages:
/tr46/0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: true
/tr46/1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
@ -14295,6 +14418,11 @@ packages:
yargs: 17.6.0
dev: true
/tunnel/0.0.6:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
dev: false
/type-check/0.3.2:
resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}
engines: {node: '>= 0.8.0'}
@ -14757,7 +14885,6 @@ packages:
/webidl-conversions/3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: true
/webidl-conversions/4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
@ -14792,7 +14919,6 @@ packages:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: true
/whatwg-url/7.1.0:
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
@ -14905,6 +15031,19 @@ packages:
engines: {node: '>=12'}
dev: true
/xml2js/0.4.23:
resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
engines: {node: '>=4.0.0'}
dependencies:
sax: 1.2.4
xmlbuilder: 11.0.1
dev: false
/xmlbuilder/11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
dev: false
/xmlchars/2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
dev: true