From 5e06ee7cc69dae3660a48cf80265462ff128c95e Mon Sep 17 00:00:00 2001 From: MagicBella <137158696+MagicBella@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:49:58 +0100 Subject: [PATCH] chore: regen sdks (#453) Automated PR to update the generated SDKs --- .../auto-bump-project-client-1734089735.md | 5 + .../auto-bump-user-client-1734089735.md | 5 + packages/project-client/README.md | 4 +- .../documentation/models/ApnsConfig.md | 16 +- .../documentation/models/ApnsToken.md | 12 +- .../documentation/models/SlackConfig.md | 12 +- .../services/BroadcastsService.md | 6 +- .../documentation/services/ChannelsService.md | 16 +- .../documentation/services/EventsService.md | 2 +- .../services/IntegrationsService.md | 31 +- .../documentation/services/JwtService.md | 8 +- packages/project-client/src/http/client.ts | 9 +- .../src/http/handlers/auth-handler.ts | 14 +- .../src/http/handlers/handler-chain.ts | 8 + .../src/http/handlers/hook-handler.ts | 23 +- .../src/http/handlers/mb-header-handler.ts | 37 -- .../handlers/request-validation-handler.ts | 19 +- .../handlers/response-validation-handler.ts | 147 +++++-- .../src/http/handlers/retry-handler.ts | 20 + .../src/http/handlers/terminating-handler.ts | 4 + .../http/transport/request-fetch-adapter.ts | 47 ++- packages/project-client/src/http/types.ts | 3 +- .../services/channels/models/apns-token.ts | 4 +- .../integrations/models/apns-config.ts | 12 +- .../integrations/models/slack-config.ts | 8 +- packages/user-client/README.md | 4 +- .../documentation/models/ApnsToken.md | 12 +- .../documentation/services/ChannelsService.md | 16 +- .../services/IntegrationsService.md | 6 +- packages/user-client/src/http/client.ts | 7 +- .../src/http/handlers/auth-handler.ts | 14 +- .../src/http/handlers/handler-chain.ts | 8 + .../src/http/handlers/hook-handler.ts | 23 +- .../handlers/request-validation-handler.ts | 45 ++- .../handlers/response-validation-handler.ts | 147 +++++-- .../src/http/handlers/retry-handler.ts | 20 + .../src/http/handlers/terminating-handler.ts | 4 + .../http/transport/request-fetch-adapter.ts | 47 ++- packages/user-client/src/http/types.ts | 2 + .../services/channels/models/apns-token.ts | 4 +- .../channels/models/token-metadata.ts | 8 +- yarn.lock | 368 +++++++++++++++++- 42 files changed, 960 insertions(+), 247 deletions(-) create mode 100644 .changeset/auto-bump-project-client-1734089735.md create mode 100644 .changeset/auto-bump-user-client-1734089735.md delete mode 100644 packages/project-client/src/http/handlers/mb-header-handler.ts diff --git a/.changeset/auto-bump-project-client-1734089735.md b/.changeset/auto-bump-project-client-1734089735.md new file mode 100644 index 000000000..7586d441a --- /dev/null +++ b/.changeset/auto-bump-project-client-1734089735.md @@ -0,0 +1,5 @@ +--- +'@magicbell/project-client': minor +--- + +Automatic minor version bump for changes in `@magicbell/project-client`. diff --git a/.changeset/auto-bump-user-client-1734089735.md b/.changeset/auto-bump-user-client-1734089735.md new file mode 100644 index 000000000..8b4ab1af4 --- /dev/null +++ b/.changeset/auto-bump-user-client-1734089735.md @@ -0,0 +1,5 @@ +--- +'@magicbell/user-client': minor +--- + +Automatic minor version bump for changes in `@magicbell/user-client`. diff --git a/packages/project-client/README.md b/packages/project-client/README.md index 2c2803cc8..f18ce6b0c 100644 --- a/packages/project-client/README.md +++ b/packages/project-client/README.md @@ -2,6 +2,8 @@ Welcome to the Client SDK documentation. This guide will help you get started with integrating and using the Client SDK in your project. +[![This SDK was generated by liblab](https://public-liblab-readme-assets.s3.us-east-1.amazonaws.com/built-by-liblab-icon.svg)](https://liblab.com/?utm_source=readme) + ## Versions - API version: `2.0.0` @@ -81,7 +83,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.broadcasts.listBroadcasts({ - pageSize: 6, + pageSize: 8, pageAfter: 'page[after]', pageBefore: 'page[before]', }); diff --git a/packages/project-client/documentation/models/ApnsConfig.md b/packages/project-client/documentation/models/ApnsConfig.md index 4a9e47e67..5a841e97b 100644 --- a/packages/project-client/documentation/models/ApnsConfig.md +++ b/packages/project-client/documentation/models/ApnsConfig.md @@ -2,14 +2,14 @@ **Properties** -| Name | Type | Required | Description | -| :------------- | :------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| appId | string | ✅ | | -| badge | Badge | ✅ | | -| certificate | string | ✅ | The APNs certificate in PEM format. Generate it at [developer.apple.com](https://developer.apple.com/account/resources/authkeys/add) with the 'Apple Push Notification service (APNs)' option selected. | -| keyId | string | ✅ | | -| teamId | string | ✅ | | -| payloadVersion | PayloadVersion | ❌ | | +| Name | Type | Required | Description | +| :------------- | :------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| appId | string | ✅ | The default bundle identifier of the application that is configured with this project. It can be overriden on a per token basis, when registering device tokens. | +| badge | Badge | ✅ | | +| certificate | string | ✅ | The APNs certificate in P8 format. Generate it at [developer.apple.com](https://developer.apple.com/account/resources/authkeys/add) with the 'Apple Push Notification service (APNs)' option selected. | +| keyId | string | ✅ | | +| teamId | string | ✅ | | +| payloadVersion | PayloadVersion | ❌ | | # Badge diff --git a/packages/project-client/documentation/models/ApnsToken.md b/packages/project-client/documentation/models/ApnsToken.md index 5a4d63a62..b9f3f5d95 100644 --- a/packages/project-client/documentation/models/ApnsToken.md +++ b/packages/project-client/documentation/models/ApnsToken.md @@ -2,14 +2,16 @@ **Properties** -| Name | Type | Required | Description | -| :------------- | :---------------------- | :------- | :---------- | -| deviceToken | string | ✅ | | -| appId | string | ❌ | | -| installationId | ApnsTokenInstallationId | ❌ | | +| Name | Type | Required | Description | +| :------------- | :---------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| deviceToken | string | ✅ | | +| appId | string | ❌ | (Optional) The bundle identifier of the application that is registering this token. Use this field to override the default identifier specified in the projects APNs integration. | +| installationId | ApnsTokenInstallationId | ❌ | (Optional) The APNs environment the token is registered for. If none is provided we assume the token is used in `production`. | # ApnsTokenInstallationId +(Optional) The APNs environment the token is registered for. If none is provided we assume the token is used in `production`. + **Properties** | Name | Type | Required | Description | diff --git a/packages/project-client/documentation/models/SlackConfig.md b/packages/project-client/documentation/models/SlackConfig.md index fe650c64e..913ed86ff 100644 --- a/packages/project-client/documentation/models/SlackConfig.md +++ b/packages/project-client/documentation/models/SlackConfig.md @@ -2,9 +2,9 @@ **Properties** -| Name | Type | Required | Description | -| :------------ | :----- | :------- | :---------- | -| appId | string | ✅ | | -| clientId | string | ✅ | | -| clientSecret | string | ✅ | | -| signingSecret | string | ✅ | | +| Name | Type | Required | Description | +| :------------ | :----- | :------- | :------------------------------------------------------------------------------------------------ | +| appId | string | ✅ | The Slack app ID that can be found in the app's settings page of the Slack API dashboard. | +| clientId | string | ✅ | The Slack client ID that can be found in the app's settings page of the Slack API dashboard. | +| clientSecret | string | ✅ | The Slack client secret that can be found in the app's settings page of the Slack API dashboard. | +| signingSecret | string | ✅ | The Slack signing secret that can be found in the app's settings page of the Slack API dashboard. | diff --git a/packages/project-client/documentation/services/BroadcastsService.md b/packages/project-client/documentation/services/BroadcastsService.md index f5cf2855d..f320be344 100644 --- a/packages/project-client/documentation/services/BroadcastsService.md +++ b/packages/project-client/documentation/services/BroadcastsService.md @@ -38,7 +38,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.broadcasts.listBroadcasts({ - pageSize: 6, + pageSize: 8, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -141,8 +141,8 @@ import { Broadcast, Client } from '@magicbell/project-client'; const statusStatus = StatusStatus.ENQUEUED; const summary: Summary = { - failures: 10, - total: 8, + failures: 2, + total: 9, }; const broadcastStatus: BroadcastStatus = { diff --git a/packages/project-client/documentation/services/ChannelsService.md b/packages/project-client/documentation/services/ChannelsService.md index 0757e61c5..47171220a 100644 --- a/packages/project-client/documentation/services/ChannelsService.md +++ b/packages/project-client/documentation/services/ChannelsService.md @@ -80,10 +80,10 @@ import { Client, ProjectDeliveryConfig } from '@magicbell/project-client'; const projectDeliveryConfigChannels: ProjectDeliveryConfigChannels = { channel: channelsChannel1, - delay: 1, + delay: 8, disabled: true, if: 'if', - priority: 5, + priority: 3, }; const projectDeliveryConfig: ProjectDeliveryConfig = { @@ -125,10 +125,10 @@ import { CategoryDeliveryConfig, Client } from '@magicbell/project-client'; const categoryDeliveryConfigChannels: CategoryDeliveryConfigChannels = { channel: channelsChannel2, - delay: 3, + delay: 6, disabled: true, if: 'if', - priority: 5, + priority: 1, }; const categoryDeliveryConfig: CategoryDeliveryConfig = { @@ -282,7 +282,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.channels.getMobilePushExpoUserTokens('user_id', { - pageSize: 2, + pageSize: 6, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -390,7 +390,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.channels.getMobilePushFcmUserTokens('user_id', { - pageSize: 3, + pageSize: 4, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -606,7 +606,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.channels.getTeamsUserTokens('user_id', { - pageSize: 123, + pageSize: 8, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -714,7 +714,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.channels.getWebPushUserTokens('user_id', { - pageSize: 9, + pageSize: 4, pageAfter: 'page[after]', pageBefore: 'page[before]', }); diff --git a/packages/project-client/documentation/services/EventsService.md b/packages/project-client/documentation/services/EventsService.md index 4dd67b15c..33d247229 100644 --- a/packages/project-client/documentation/services/EventsService.md +++ b/packages/project-client/documentation/services/EventsService.md @@ -36,7 +36,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.events.getEvents({ - pageSize: 1, + pageSize: 8, pageAfter: 'page[after]', pageBefore: 'page[before]', }); diff --git a/packages/project-client/documentation/services/IntegrationsService.md b/packages/project-client/documentation/services/IntegrationsService.md index a7ad2cd3c..4723f48ab 100644 --- a/packages/project-client/documentation/services/IntegrationsService.md +++ b/packages/project-client/documentation/services/IntegrationsService.md @@ -96,7 +96,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.integrations.listIntegrations({ - pageSize: 8, + pageSize: 3, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -165,13 +165,13 @@ const payloadVersion = PayloadVersion._1; const apnsConfig: ApnsConfig = { appId: "app_id", badge: badge, - certificate: "--------- BEGINF-------- -DufxW= -------- ENDQPJ--- + certificate: "----- BEGIN PRIVATE KEY---------- +42qmz9= +---- END PRIVATE KEY- ", - keyId: "et in elit", + keyId: "mollit dol", payloadVersion: payloadVersion, - teamId: "consequat " + teamId: "enimmollit" }; const { data } = await client.integrations.saveApnsIntegration( @@ -527,9 +527,10 @@ const fcmConfig: FcmConfig = { clientEmail: "client_email", clientId: "client_id", clientX509CertUrl: "client_x509_cert_url", - privateKey: "BEGINUQYD-------- -rSm== --------ENDTYIWA---", + privateKey: "BEGINNQX-------- +B+JvPM/f7Ms== +------ENDEPJVIYYY----- +", privateKeyId: "private_key_id", projectId: "project_id", tokenUri: "token_uri", @@ -773,7 +774,7 @@ import { Client, InboxConfig } from '@magicbell/project-client'; const banner: Banner = { backgroundColor: 'backgroundColor', - backgroundOpacity: 3.98, + backgroundOpacity: 4.92, fontSize: 'fontSize', textColor: 'textColor', }; @@ -1481,10 +1482,10 @@ import { Client, SlackConfig } from '@magicbell/project-client'; }); const slackConfig: SlackConfig = { - appId: 'QPB3C3EYQY4', - clientId: '01131.219', - clientSecret: 'mollit Ut pariaturex ut Duis dol', - signingSecret: 'officia commodoincididunt quisel', + appId: 'QTEIRE', + clientId: '7201.6182620', + clientSecret: 'ex dolore ametauteDuis culpaquis', + signingSecret: 'eu ex animmollit sunt consequat ', }; const { data } = await client.integrations.saveSlackIntegration(slackConfig); @@ -1835,7 +1836,7 @@ import { Client, TwilioConfig } from '@magicbell/project-client'; accountSid: 'account_sid', apiKey: 'api_key', apiSecret: 'api_secret', - from: '+65321207213408', + from: '+0528138710599', region: twilioConfigRegion, }; diff --git a/packages/project-client/documentation/services/JwtService.md b/packages/project-client/documentation/services/JwtService.md index 3e496d8da..58dfd3b4e 100644 --- a/packages/project-client/documentation/services/JwtService.md +++ b/packages/project-client/documentation/services/JwtService.md @@ -41,7 +41,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.jwt.fetchProjectTokens({ - pageSize: 10, + pageSize: 5, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -78,7 +78,7 @@ import { Client, CreateProjectTokenRequest } from '@magicbell/project-client'; }); const createProjectTokenRequest: CreateProjectTokenRequest = { - expiry: 5, + expiry: 8, name: 'name', }; @@ -150,7 +150,7 @@ import { Client, CreateUserTokenRequest } from '@magicbell/project-client'; const createUserTokenRequest: CreateUserTokenRequest = { email: 'email', - expiry: 6, + expiry: 8, externalId: 'external_id', name: 'name', }; @@ -225,7 +225,7 @@ import { Client } from '@magicbell/project-client'; }); const { data } = await client.jwt.fetchUserTokens('user_id', { - pageSize: 9, + pageSize: 3, pageAfter: 'page[after]', pageBefore: 'page[before]', }); diff --git a/packages/project-client/src/http/client.ts b/packages/project-client/src/http/client.ts index 7b97524ee..597626168 100644 --- a/packages/project-client/src/http/client.ts +++ b/packages/project-client/src/http/client.ts @@ -1,21 +1,18 @@ import { AuthHandler } from './handlers/auth-handler.js'; import { RequestHandlerChain } from './handlers/handler-chain.js'; import { HookHandler } from './handlers/hook-handler.js'; -import { HeaderHandler } from './handlers/mb-header-handler.js'; import { RequestValidationHandler } from './handlers/request-validation-handler.js'; import { ResponseValidationHandler } from './handlers/response-validation-handler.js'; import { RetryHandler } from './handlers/retry-handler.js'; import { TerminatingHandler } from './handlers/terminating-handler.js'; import { CustomHook } from './hooks/custom-hook.js'; -import { SerializationStyle } from './serialization/base-serializer.js'; import { Request } from './transport/request.js'; -import { HttpMethod, HttpResponse, Options, RetryOptions, SdkConfig } from './types.js'; +import { HttpResponse, SdkConfig } from './types.js'; export class HttpClient { private readonly requestHandlerChain = new RequestHandlerChain(); constructor(private config: SdkConfig, hook = new CustomHook()) { - this.requestHandlerChain.addHandler(new HeaderHandler()); this.requestHandlerChain.addHandler(new ResponseValidationHandler()); this.requestHandlerChain.addHandler(new RequestValidationHandler()); this.requestHandlerChain.addHandler(new AuthHandler()); @@ -28,6 +25,10 @@ export class HttpClient { return this.requestHandlerChain.callChain(request); } + async *stream(request: Request): AsyncGenerator> { + yield* this.requestHandlerChain.streamChain(request); + } + public async callPaginated(request: Request): Promise> { const response = await this.call(request as any); diff --git a/packages/project-client/src/http/handlers/auth-handler.ts b/packages/project-client/src/http/handlers/auth-handler.ts index cbc35bfaa..8fcf99d64 100644 --- a/packages/project-client/src/http/handlers/auth-handler.ts +++ b/packages/project-client/src/http/handlers/auth-handler.ts @@ -5,14 +5,24 @@ import { HttpResponse, RequestHandler } from '../types.js'; export class AuthHandler implements RequestHandler { next?: RequestHandler; - async handle(request: Request): Promise> { + public async handle(request: Request): Promise> { const requestWithAuth = this.addAccessTokenHeader(request); if (!this.next) { throw new Error(`No next handler set in ${AuthHandler.name}`); } - return this.next?.handle(requestWithAuth); + return this.next.handle(requestWithAuth); + } + + public async *stream(request: Request): AsyncGenerator> { + const requestWithAuth = this.addAccessTokenHeader(request); + + if (!this.next) { + throw new Error(`No next handler set in ${AuthHandler.name}`); + } + + yield* this.next.stream(requestWithAuth); } private addAccessTokenHeader(request: Request): Request { diff --git a/packages/project-client/src/http/handlers/handler-chain.ts b/packages/project-client/src/http/handlers/handler-chain.ts index 1729c81bc..7f55f13db 100644 --- a/packages/project-client/src/http/handlers/handler-chain.ts +++ b/packages/project-client/src/http/handlers/handler-chain.ts @@ -19,4 +19,12 @@ export class RequestHandlerChain { return this.handlers[0].handle(request); } + + async *streamChain(request: Request): AsyncGenerator> { + if (!this.handlers.length) { + throw new Error('No handlers added to the chain'); + } + + yield* this.handlers[0].stream(request); + } } diff --git a/packages/project-client/src/http/handlers/hook-handler.ts b/packages/project-client/src/http/handlers/hook-handler.ts index 519adb231..0850a72c1 100644 --- a/packages/project-client/src/http/handlers/hook-handler.ts +++ b/packages/project-client/src/http/handlers/hook-handler.ts @@ -1,4 +1,3 @@ -import { HttpError } from '../error.js'; import { Hook } from '../hooks/hook.js'; import { Request } from '../transport/request.js'; import { TransportHookAdapter } from '../transport/transport-hook-adapter.js'; @@ -29,6 +28,28 @@ export class HookHandler implements RequestHandler { throw await hook.onError(nextRequest, response, hookParams); } + async *stream(request: Request): AsyncGenerator> { + if (!this.next) { + throw new Error('No next handler set in hook handler.'); + } + + const hook = new TransportHookAdapter(); + + const hookParams = this.getHookParams(request); + + const nextRequest = await hook.beforeRequest(request, hookParams); + + const stream = this.next.stream(nextRequest); + + for await (const response of stream) { + if (response.metadata.status < 400) { + yield await hook.afterResponse(nextRequest, response, hookParams); + } else { + throw await hook.onError(nextRequest, response, hookParams); + } + } + } + private getHookParams(_request: Request): Map { const hookParams: Map = new Map(); return hookParams; diff --git a/packages/project-client/src/http/handlers/mb-header-handler.ts b/packages/project-client/src/http/handlers/mb-header-handler.ts deleted file mode 100644 index 7611455c2..000000000 --- a/packages/project-client/src/http/handlers/mb-header-handler.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { SerializationStyle } from '../serialization/base-serializer.js'; -import { Request } from '../transport/request.js'; -import { HttpResponse, RequestHandler } from '../types.js'; - -export class HeaderHandler implements RequestHandler { - next?: RequestHandler; - - async handle(request: Request): Promise> { - if (!this.next) { - throw new Error(`No next handler set in ${HeaderHandler.name}`); - } - - const requestWithHeaders = this.addHeaders(request); - return this.next?.handle(requestWithHeaders); - } - - private addHeaders(request: Request): Request { - const { headers } = request.config as any; - if (typeof headers !== 'object') { - return request; - } - - for (const [key, value] of Object.entries(headers)) { - request.addHeaderParam(key, { - key: key, - value: value, - explode: false, - encode: false, - style: SerializationStyle.SIMPLE, - isLimit: false, - isOffset: false, - }); - } - - return request; - } -} diff --git a/packages/project-client/src/http/handlers/request-validation-handler.ts b/packages/project-client/src/http/handlers/request-validation-handler.ts index c435863cf..b35511fa9 100644 --- a/packages/project-client/src/http/handlers/request-validation-handler.ts +++ b/packages/project-client/src/http/handlers/request-validation-handler.ts @@ -1,4 +1,3 @@ -import { HttpError } from '../error.js'; import { Request } from '../transport/request.js'; import { ContentType, HttpResponse, RequestHandler } from '../types.js'; @@ -10,6 +9,22 @@ export class RequestValidationHandler implements RequestHandler { throw new Error('No next handler set in ContentTypeHandler.'); } + this.validateRequest(request); + + return this.next.handle(request); + } + + async *stream(request: Request): AsyncGenerator> { + if (!this.next) { + throw new Error('No next handler set in ContentTypeHandler.'); + } + + this.validateRequest(request); + + yield* this.next.stream(request); + } + + validateRequest(request: Request): void { if (request.requestContentType === ContentType.Json) { request.body = JSON.stringify(request.requestSchema?.parse(request.body)); } else if ( @@ -25,8 +40,6 @@ export class RequestValidationHandler implements RequestHandler { } else { request.body = JSON.stringify(request.requestSchema?.parse(request.body)); } - - return await this.next.handle(request); } toFormUrlEncoded(request: Request): string { diff --git a/packages/project-client/src/http/handlers/response-validation-handler.ts b/packages/project-client/src/http/handlers/response-validation-handler.ts index a2fade743..d1fa7370a 100644 --- a/packages/project-client/src/http/handlers/response-validation-handler.ts +++ b/packages/project-client/src/http/handlers/response-validation-handler.ts @@ -1,6 +1,5 @@ import { ZodUndefined } from 'zod'; -import { HttpError } from '../error.js'; import { Request } from '../transport/request.js'; import { ContentType, HttpResponse, RequestHandler } from '../types.js'; @@ -10,51 +9,119 @@ export class ResponseValidationHandler implements RequestHandler { async handle(request: Request): Promise> { const response = await this.next!.handle(request); + return this.decodeBody(request, response); + } + + async *stream(request: Request): AsyncGenerator> { + const stream = this.next!.stream(request); + + for await (const response of stream) { + const responseChunks = this.splitByDataChunks(response); + for (const chunk of responseChunks) { + yield this.decodeBody(request, chunk); + } + } + } + + private splitByDataChunks(response: HttpResponse): HttpResponse[] { + if (!response.metadata.headers['content-type'].includes('text/event-stream')) { + return [response]; + } + + const text = new TextDecoder().decode(response.raw); + const encoder = new TextEncoder(); + return text + .split('\n') + .filter((line) => line.startsWith('data: ')) + .map((part) => ({ + ...response, + raw: encoder.encode(part), + })); + } + + private decodeBody(request: Request, response: HttpResponse): HttpResponse { if (!this.hasContent(request, response)) { return response; } - if (request.responseContentType === ContentType.Json) { - const decodedBody = new TextDecoder().decode(response.raw); - const json = JSON.parse(decodedBody); - return { - ...response, - data: this.validate(request, json), - }; - } else if ( - request.responseContentType === ContentType.Binary || - request.responseContentType === ContentType.Image + if (request.responseContentType === ContentType.Binary || request.responseContentType === ContentType.Image) { + return this.decodeFile(request, response); + } + + if (request.responseContentType === ContentType.MultipartFormData) { + return this.decodeMultipartFormData(request, response); + } + + if (request.responseContentType === ContentType.Text || request.responseContentType === ContentType.Xml) { + return this.decodeText(request, response); + } + + if (request.responseContentType === ContentType.FormUrlEncoded) { + return this.decodeFormUrlEncoded(request, response); + } + + if ( + request.responseContentType === ContentType.EventStream || + response.metadata.headers['content-type'].includes('text/event-stream') ) { - return { - ...response, - data: this.validate(request, response.raw), - }; - } else if (request.responseContentType === ContentType.Text || request.responseContentType === ContentType.Xml) { - const text = new TextDecoder().decode(response.raw); - return { - ...response, - data: this.validate(request, text), - }; - } else if (request.responseContentType === ContentType.FormUrlEncoded) { - const urlEncoded = this.fromUrlEncoded(new TextDecoder().decode(response.raw)); - return { - ...response, - data: this.validate(request, urlEncoded), - }; - } else if (request.responseContentType === ContentType.MultipartFormData) { - const formData = this.fromFormData(response.raw); - return { - ...response, - data: this.validate(request, formData), - }; - } else { - const decodedBody = new TextDecoder().decode(response.raw); - const json = JSON.parse(decodedBody); - return { - ...response, - data: this.validate(request, json), - }; + return this.decodeEventStream(request, response); + } + + return this.decodeJson(request, response); + } + + private decodeFile(request: Request, response: HttpResponse): HttpResponse { + return { + ...response, + data: this.validate(request, response.raw), + }; + } + + private decodeMultipartFormData(request: Request, response: HttpResponse): HttpResponse { + const formData = this.fromFormData(response.raw); + return { + ...response, + data: this.validate(request, formData), + }; + } + + private decodeText(request: Request, response: HttpResponse): HttpResponse { + const decodedBody = new TextDecoder().decode(response.raw); + return { + ...response, + data: this.validate(request, decodedBody), + }; + } + + private decodeFormUrlEncoded(request: Request, response: HttpResponse): HttpResponse { + const decodedBody = new TextDecoder().decode(response.raw); + const urlEncoded = this.fromUrlEncoded(decodedBody); + return { + ...response, + data: this.validate(request, urlEncoded), + }; + } + + private decodeEventStream(request: Request, response: HttpResponse): HttpResponse { + let decodedBody = new TextDecoder().decode(response.raw); + if (decodedBody.startsWith('data: ')) { + decodedBody = decodedBody.substring(6); } + // Note: this assumes that the content of data is a valid JSON string + const json = JSON.parse(decodedBody); + return { + ...response, + data: this.validate(request, json), + }; + } + + private decodeJson(request: Request, response: HttpResponse): HttpResponse { + const decodedBody = new TextDecoder().decode(response.raw); + const json = JSON.parse(decodedBody); + return { + ...response, + data: this.validate(request, json), + }; } private validate(request: Request, data: any): T { diff --git a/packages/project-client/src/http/handlers/retry-handler.ts b/packages/project-client/src/http/handlers/retry-handler.ts index bae8b6af0..e9ac00351 100644 --- a/packages/project-client/src/http/handlers/retry-handler.ts +++ b/packages/project-client/src/http/handlers/retry-handler.ts @@ -24,6 +24,26 @@ export class RetryHandler implements RequestHandler { throw new Error('Error retrying request.'); } + async *stream(request: Request): AsyncGenerator> { + if (!this.next) { + throw new Error('No next handler set in retry handler.'); + } + + for (let attempt = 1; attempt <= request.retry.attempts; attempt++) { + try { + yield* this.next.stream(request); + return; + } catch (error: any) { + if (!this.shouldRetry(error) || attempt === request.retry.attempts) { + throw error; + } + await this.delay(request.retry.delayMs); + } + } + + throw new Error('Error retrying request.'); + } + private shouldRetry(error: Error): boolean { return error instanceof HttpError && (error.metadata.status >= 500 || error.metadata.status === 408); } diff --git a/packages/project-client/src/http/handlers/terminating-handler.ts b/packages/project-client/src/http/handlers/terminating-handler.ts index 27222b487..43c7b77af 100644 --- a/packages/project-client/src/http/handlers/terminating-handler.ts +++ b/packages/project-client/src/http/handlers/terminating-handler.ts @@ -7,4 +7,8 @@ export class TerminatingHandler implements RequestHandler { async handle(request: Request): Promise> { return new RequestFetchAdapter(request).send(); } + + async *stream(request: Request): AsyncGenerator> { + yield* new RequestFetchAdapter(request).stream(); + } } diff --git a/packages/project-client/src/http/transport/request-fetch-adapter.ts b/packages/project-client/src/http/transport/request-fetch-adapter.ts index f36b89e80..30f04f80e 100644 --- a/packages/project-client/src/http/transport/request-fetch-adapter.ts +++ b/packages/project-client/src/http/transport/request-fetch-adapter.ts @@ -1,9 +1,10 @@ import { HttpError } from '../error.js'; -import { HttpMethod, HttpResponse } from '../types.js'; +import { HttpMetadata, HttpMethod, HttpResponse } from '../types.js'; import { Request } from './request.js'; interface HttpAdapter { send(): Promise; + stream(): AsyncGenerator; } export class RequestFetchAdapter implements HttpAdapter { @@ -19,7 +20,7 @@ export class RequestFetchAdapter implements HttpAdapter { public async send(): Promise> { const response = await fetch(this.request.constructFullUrl(), this.requestInit); - const metadata = { + const metadata: HttpMetadata = { status: response.status, statusText: response.statusText || '', headers: this.getHeaders(response), @@ -31,7 +32,41 @@ export class RequestFetchAdapter implements HttpAdapter { }; } - private setMethod(method: HttpMethod) { + public async *stream(): AsyncGenerator> { + const response = await fetch(this.request.constructFullUrl(), this.requestInit); + + const metadata: HttpMetadata = { + status: response.status, + statusText: response.statusText || '', + headers: this.getHeaders(response), + }; + + if (response.status >= 400) { + throw new HttpError(metadata, await response.clone().arrayBuffer()); + } + + if (!response.body) { + return yield { + metadata, + raw: await response.clone().arrayBuffer(), + }; + } + + const reader = response.body.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + yield { + metadata, + raw: value, + }; + } + } + + private setMethod(method: HttpMethod): void { if (!method) { return; } @@ -41,7 +76,7 @@ export class RequestFetchAdapter implements HttpAdapter { }; } - private setBody(body: ReadableStream | null) { + private setBody(body: ReadableStream | null): void { if (!body) { return; } @@ -51,7 +86,7 @@ export class RequestFetchAdapter implements HttpAdapter { }; } - private setHeaders(headers: HeadersInit | undefined) { + private setHeaders(headers: HeadersInit | undefined): void { if (!headers) { return; } @@ -62,7 +97,7 @@ export class RequestFetchAdapter implements HttpAdapter { }; } - private setTimeout(timeoutMs: number | undefined) { + private setTimeout(timeoutMs: number | undefined): void { if (!timeoutMs) { return; } diff --git a/packages/project-client/src/http/types.ts b/packages/project-client/src/http/types.ts index 9fb29df4a..fb17dafc7 100644 --- a/packages/project-client/src/http/types.ts +++ b/packages/project-client/src/http/types.ts @@ -12,7 +12,6 @@ export interface SdkConfig { token?: string; retry?: RetryOptions; validation?: ValidationOptions; - headers?: Record; } export interface HttpMetadata { @@ -31,6 +30,7 @@ export interface RequestHandler { next?: RequestHandler; handle(request: Request): Promise>; + stream(request: Request): AsyncGenerator>; } export enum ContentType { @@ -43,6 +43,7 @@ export enum ContentType { FormUrlEncoded = 'form', Text = 'text', MultipartFormData = 'multipartFormData', + EventStream = 'eventStream', } export interface Options { diff --git a/packages/project-client/src/services/channels/models/apns-token.ts b/packages/project-client/src/services/channels/models/apns-token.ts index 2bffe8b54..b5a4815a5 100644 --- a/packages/project-client/src/services/channels/models/apns-token.ts +++ b/packages/project-client/src/services/channels/models/apns-token.ts @@ -17,9 +17,9 @@ export const apnsToken = z.lazy(() => { /** * * @typedef {ApnsToken} apnsToken + * @property {string} - (Optional) The bundle identifier of the application that is registering this token. Use this field to override the default identifier specified in the projects APNs integration. * @property {string} - * @property {string} - * @property {ApnsTokenInstallationId} + * @property {ApnsTokenInstallationId} - (Optional) The APNs environment the token is registered for. If none is provided we assume the token is used in `production`. */ export type ApnsToken = z.infer; diff --git a/packages/project-client/src/services/integrations/models/apns-config.ts b/packages/project-client/src/services/integrations/models/apns-config.ts index 8d7f2b334..a4201034e 100644 --- a/packages/project-client/src/services/integrations/models/apns-config.ts +++ b/packages/project-client/src/services/integrations/models/apns-config.ts @@ -7,7 +7,9 @@ export const apnsConfig = z.lazy(() => { return z.object({ appId: z.string().regex(/^[a-zA-Z0-9]+(.[a-zA-Z0-9]+)*$/), badge: z.string(), - certificate: z.string().regex(/^-+?\s?BEGIN[A-Z ]+-+\n([A-Za-z0-9+\/\r\n]+={0,2})\n-+\s?END[A-Z ]+-+\n?$/), + certificate: z + .string() + .regex(/^-+?\s?BEGIN PRIVATE KEY-+\n([A-Za-z0-9+\/\r\n]+={0,2})\n-+\s?END PRIVATE KEY+-+\n?$/), keyId: z.string().min(10).max(10), payloadVersion: z.string().optional(), teamId: z.string().min(10).max(10), @@ -17,9 +19,9 @@ export const apnsConfig = z.lazy(() => { /** * * @typedef {ApnsConfig} apnsConfig - * @property {string} + * @property {string} - The default bundle identifier of the application that is configured with this project. It can be overriden on a per token basis, when registering device tokens. * @property {Badge} - * @property {string} - The APNs certificate in PEM format. Generate it at [developer.apple.com](https://developer.apple.com/account/resources/authkeys/add) with the 'Apple Push Notification service (APNs)' option selected. + * @property {string} - The APNs certificate in P8 format. Generate it at [developer.apple.com](https://developer.apple.com/account/resources/authkeys/add) with the 'Apple Push Notification service (APNs)' option selected. * @property {string} * @property {PayloadVersion} * @property {string} @@ -35,7 +37,9 @@ export const apnsConfigResponse = z.lazy(() => { .object({ app_id: z.string().regex(/^[a-zA-Z0-9]+(.[a-zA-Z0-9]+)*$/), badge: z.string(), - certificate: z.string().regex(/^-+?\s?BEGIN[A-Z ]+-+\n([A-Za-z0-9+\/\r\n]+={0,2})\n-+\s?END[A-Z ]+-+\n?$/), + certificate: z + .string() + .regex(/^-+?\s?BEGIN PRIVATE KEY-+\n([A-Za-z0-9+\/\r\n]+={0,2})\n-+\s?END PRIVATE KEY+-+\n?$/), key_id: z.string().min(10).max(10), payload_version: z.string().optional(), team_id: z.string().min(10).max(10), diff --git a/packages/project-client/src/services/integrations/models/slack-config.ts b/packages/project-client/src/services/integrations/models/slack-config.ts index 1cee92af6..46355dc47 100644 --- a/packages/project-client/src/services/integrations/models/slack-config.ts +++ b/packages/project-client/src/services/integrations/models/slack-config.ts @@ -15,10 +15,10 @@ export const slackConfig = z.lazy(() => { /** * * @typedef {SlackConfig} slackConfig - * @property {string} - * @property {string} - * @property {string} - * @property {string} + * @property {string} - The Slack app ID that can be found in the app's settings page of the Slack API dashboard. + * @property {string} - The Slack client ID that can be found in the app's settings page of the Slack API dashboard. + * @property {string} - The Slack client secret that can be found in the app's settings page of the Slack API dashboard. + * @property {string} - The Slack signing secret that can be found in the app's settings page of the Slack API dashboard. */ export type SlackConfig = z.infer; diff --git a/packages/user-client/README.md b/packages/user-client/README.md index e8710007e..f0e0c69db 100644 --- a/packages/user-client/README.md +++ b/packages/user-client/README.md @@ -2,6 +2,8 @@ Welcome to the Client SDK documentation. This guide will help you get started with integrating and using the Client SDK in your project. +[![This SDK was generated by liblab](https://public-liblab-readme-assets.s3.us-east-1.amazonaws.com/built-by-liblab-icon.svg)](https://liblab.com/?utm_source=readme) + ## Versions - API version: `2.0.0` @@ -81,7 +83,7 @@ import { Client } from '@magicbell/user-client'; }); const { data } = await client.channels.getMobilePushApnsTokens({ - pageSize: 9, + pageSize: 6, pageAfter: 'page[after]', pageBefore: 'page[before]', }); diff --git a/packages/user-client/documentation/models/ApnsToken.md b/packages/user-client/documentation/models/ApnsToken.md index 5a4d63a62..b9f3f5d95 100644 --- a/packages/user-client/documentation/models/ApnsToken.md +++ b/packages/user-client/documentation/models/ApnsToken.md @@ -2,14 +2,16 @@ **Properties** -| Name | Type | Required | Description | -| :------------- | :---------------------- | :------- | :---------- | -| deviceToken | string | ✅ | | -| appId | string | ❌ | | -| installationId | ApnsTokenInstallationId | ❌ | | +| Name | Type | Required | Description | +| :------------- | :---------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| deviceToken | string | ✅ | | +| appId | string | ❌ | (Optional) The bundle identifier of the application that is registering this token. Use this field to override the default identifier specified in the projects APNs integration. | +| installationId | ApnsTokenInstallationId | ❌ | (Optional) The APNs environment the token is registered for. If none is provided we assume the token is used in `production`. | # ApnsTokenInstallationId +(Optional) The APNs environment the token is registered for. If none is provided we assume the token is used in `production`. + **Properties** | Name | Type | Required | Description | diff --git a/packages/user-client/documentation/services/ChannelsService.md b/packages/user-client/documentation/services/ChannelsService.md index 0cd526ec6..a7b181d97 100644 --- a/packages/user-client/documentation/services/ChannelsService.md +++ b/packages/user-client/documentation/services/ChannelsService.md @@ -59,7 +59,7 @@ import { Client } from '@magicbell/user-client'; }); const { data } = await client.channels.getMobilePushApnsTokens({ - pageSize: 9, + pageSize: 6, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -99,7 +99,7 @@ import { ApnsToken, Client } from '@magicbell/user-client'; const apnsToken: ApnsToken = { appId: 'app_id', - deviceToken: 'voluptateveniam', + deviceToken: 'voluptate paria', installationId: apnsTokenInstallationId, }; @@ -205,7 +205,7 @@ import { Client } from '@magicbell/user-client'; }); const { data } = await client.channels.getMobilePushExpoTokens({ - pageSize: 4, + pageSize: 6, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -347,7 +347,7 @@ import { Client } from '@magicbell/user-client'; }); const { data } = await client.channels.getMobilePushFcmTokens({ - pageSize: 2, + pageSize: 10, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -386,7 +386,7 @@ import { Client, FcmToken } from '@magicbell/user-client'; const fcmTokenInstallationId = FcmTokenInstallationId.DEVELOPMENT; const fcmToken: FcmToken = { - deviceToken: 'pariaturaliqua ', + deviceToken: 'eiusmod esse ni', installationId: fcmTokenInstallationId, }; @@ -492,7 +492,7 @@ import { Client } from '@magicbell/user-client'; }); const { data } = await client.channels.getSlackTokens({ - pageSize: 10, + pageSize: 2, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -645,7 +645,7 @@ import { Client } from '@magicbell/user-client'; }); const { data } = await client.channels.getTeamsTokens({ - pageSize: 9, + pageSize: 2, pageAfter: 'page[after]', pageBefore: 'page[before]', }); @@ -791,7 +791,7 @@ import { Client } from '@magicbell/user-client'; }); const { data } = await client.channels.getWebPushTokens({ - pageSize: 7, + pageSize: 5, pageAfter: 'page[after]', pageBefore: 'page[before]', }); diff --git a/packages/user-client/documentation/services/IntegrationsService.md b/packages/user-client/documentation/services/IntegrationsService.md index 670960393..2e151e4fc 100644 --- a/packages/user-client/documentation/services/IntegrationsService.md +++ b/packages/user-client/documentation/services/IntegrationsService.md @@ -46,7 +46,7 @@ import { Client, InboxConfig } from '@magicbell/user-client'; const banner: Banner = { backgroundColor: 'backgroundColor', - backgroundOpacity: 2.68, + backgroundOpacity: 9.57, fontSize: 'fontSize', textColor: 'textColor', }; @@ -214,7 +214,7 @@ import { Client, SlackInstallation } from '@magicbell/user-client'; const authedUser: AuthedUser = { accessToken: 'access_token', - expiresIn: 123, + expiresIn: 7, id: 'id', refreshToken: 'refresh_token', scope: 'scope', @@ -244,7 +244,7 @@ import { Client, SlackInstallation } from '@magicbell/user-client'; botUserId: 'bot_user_id', enterprise: enterprise, expiresIn: 10, - id: '0GKF', + id: 'YDW09447', incomingWebhook: incomingWebhook, isEnterpriseInstall: true, refreshToken: 'refresh_token', diff --git a/packages/user-client/src/http/client.ts b/packages/user-client/src/http/client.ts index 1d7be854f..597626168 100644 --- a/packages/user-client/src/http/client.ts +++ b/packages/user-client/src/http/client.ts @@ -6,9 +6,8 @@ import { ResponseValidationHandler } from './handlers/response-validation-handle import { RetryHandler } from './handlers/retry-handler.js'; import { TerminatingHandler } from './handlers/terminating-handler.js'; import { CustomHook } from './hooks/custom-hook.js'; -import { SerializationStyle } from './serialization/base-serializer.js'; import { Request } from './transport/request.js'; -import { HttpMethod, HttpResponse, Options, RetryOptions, SdkConfig } from './types.js'; +import { HttpResponse, SdkConfig } from './types.js'; export class HttpClient { private readonly requestHandlerChain = new RequestHandlerChain(); @@ -26,6 +25,10 @@ export class HttpClient { return this.requestHandlerChain.callChain(request); } + async *stream(request: Request): AsyncGenerator> { + yield* this.requestHandlerChain.streamChain(request); + } + public async callPaginated(request: Request): Promise> { const response = await this.call(request as any); diff --git a/packages/user-client/src/http/handlers/auth-handler.ts b/packages/user-client/src/http/handlers/auth-handler.ts index cbc35bfaa..8fcf99d64 100644 --- a/packages/user-client/src/http/handlers/auth-handler.ts +++ b/packages/user-client/src/http/handlers/auth-handler.ts @@ -5,14 +5,24 @@ import { HttpResponse, RequestHandler } from '../types.js'; export class AuthHandler implements RequestHandler { next?: RequestHandler; - async handle(request: Request): Promise> { + public async handle(request: Request): Promise> { const requestWithAuth = this.addAccessTokenHeader(request); if (!this.next) { throw new Error(`No next handler set in ${AuthHandler.name}`); } - return this.next?.handle(requestWithAuth); + return this.next.handle(requestWithAuth); + } + + public async *stream(request: Request): AsyncGenerator> { + const requestWithAuth = this.addAccessTokenHeader(request); + + if (!this.next) { + throw new Error(`No next handler set in ${AuthHandler.name}`); + } + + yield* this.next.stream(requestWithAuth); } private addAccessTokenHeader(request: Request): Request { diff --git a/packages/user-client/src/http/handlers/handler-chain.ts b/packages/user-client/src/http/handlers/handler-chain.ts index 1729c81bc..7f55f13db 100644 --- a/packages/user-client/src/http/handlers/handler-chain.ts +++ b/packages/user-client/src/http/handlers/handler-chain.ts @@ -19,4 +19,12 @@ export class RequestHandlerChain { return this.handlers[0].handle(request); } + + async *streamChain(request: Request): AsyncGenerator> { + if (!this.handlers.length) { + throw new Error('No handlers added to the chain'); + } + + yield* this.handlers[0].stream(request); + } } diff --git a/packages/user-client/src/http/handlers/hook-handler.ts b/packages/user-client/src/http/handlers/hook-handler.ts index 519adb231..0850a72c1 100644 --- a/packages/user-client/src/http/handlers/hook-handler.ts +++ b/packages/user-client/src/http/handlers/hook-handler.ts @@ -1,4 +1,3 @@ -import { HttpError } from '../error.js'; import { Hook } from '../hooks/hook.js'; import { Request } from '../transport/request.js'; import { TransportHookAdapter } from '../transport/transport-hook-adapter.js'; @@ -29,6 +28,28 @@ export class HookHandler implements RequestHandler { throw await hook.onError(nextRequest, response, hookParams); } + async *stream(request: Request): AsyncGenerator> { + if (!this.next) { + throw new Error('No next handler set in hook handler.'); + } + + const hook = new TransportHookAdapter(); + + const hookParams = this.getHookParams(request); + + const nextRequest = await hook.beforeRequest(request, hookParams); + + const stream = this.next.stream(nextRequest); + + for await (const response of stream) { + if (response.metadata.status < 400) { + yield await hook.afterResponse(nextRequest, response, hookParams); + } else { + throw await hook.onError(nextRequest, response, hookParams); + } + } + } + private getHookParams(_request: Request): Map { const hookParams: Map = new Map(); return hookParams; diff --git a/packages/user-client/src/http/handlers/request-validation-handler.ts b/packages/user-client/src/http/handlers/request-validation-handler.ts index d04c9417e..b35511fa9 100644 --- a/packages/user-client/src/http/handlers/request-validation-handler.ts +++ b/packages/user-client/src/http/handlers/request-validation-handler.ts @@ -1,4 +1,3 @@ -import { HttpError } from '../error.js'; import { Request } from '../transport/request.js'; import { ContentType, HttpResponse, RequestHandler } from '../types.js'; @@ -10,6 +9,22 @@ export class RequestValidationHandler implements RequestHandler { throw new Error('No next handler set in ContentTypeHandler.'); } + this.validateRequest(request); + + return this.next.handle(request); + } + + async *stream(request: Request): AsyncGenerator> { + if (!this.next) { + throw new Error('No next handler set in ContentTypeHandler.'); + } + + this.validateRequest(request); + + yield* this.next.stream(request); + } + + validateRequest(request: Request): void { if (request.requestContentType === ContentType.Json) { request.body = JSON.stringify(request.requestSchema?.parse(request.body)); } else if ( @@ -19,41 +34,41 @@ export class RequestValidationHandler implements RequestHandler { ) { request.body = request.body; } else if (request.requestContentType === ContentType.FormUrlEncoded) { - request.body = this.toFormUrlEncoded(request.body); + request.body = this.toFormUrlEncoded(request); } else if (request.requestContentType === ContentType.MultipartFormData) { request.body = this.toFormData(request.body); } else { request.body = JSON.stringify(request.requestSchema?.parse(request.body)); } - - return await this.next.handle(request); } - toFormUrlEncoded(body: BodyInit | undefined): string { - if (body === undefined) { + toFormUrlEncoded(request: Request): string { + if (request.body === undefined) { return ''; } - if (typeof body === 'string') { - return body; + if (typeof request.body === 'string') { + return request.body; } - if (body instanceof URLSearchParams) { - return body.toString(); + if (request.body instanceof URLSearchParams) { + return request.body.toString(); } - if (body instanceof FormData) { + const validatedBody = request.requestSchema?.parse(request.body); + + if (validatedBody instanceof FormData) { const params = new URLSearchParams(); - body.forEach((value, key) => { + validatedBody.forEach((value, key) => { params.append(key, value.toString()); }); return params.toString(); } - if (typeof body === 'object' && !Array.isArray(body)) { + if (typeof validatedBody === 'object' && !Array.isArray(validatedBody)) { const params = new URLSearchParams(); - for (const [key, value] of Object.entries(body)) { - params.append(key, value.toString()); + for (const [key, value] of Object.entries(validatedBody)) { + params.append(key, `${value}`); } return params.toString(); } diff --git a/packages/user-client/src/http/handlers/response-validation-handler.ts b/packages/user-client/src/http/handlers/response-validation-handler.ts index a2fade743..d1fa7370a 100644 --- a/packages/user-client/src/http/handlers/response-validation-handler.ts +++ b/packages/user-client/src/http/handlers/response-validation-handler.ts @@ -1,6 +1,5 @@ import { ZodUndefined } from 'zod'; -import { HttpError } from '../error.js'; import { Request } from '../transport/request.js'; import { ContentType, HttpResponse, RequestHandler } from '../types.js'; @@ -10,51 +9,119 @@ export class ResponseValidationHandler implements RequestHandler { async handle(request: Request): Promise> { const response = await this.next!.handle(request); + return this.decodeBody(request, response); + } + + async *stream(request: Request): AsyncGenerator> { + const stream = this.next!.stream(request); + + for await (const response of stream) { + const responseChunks = this.splitByDataChunks(response); + for (const chunk of responseChunks) { + yield this.decodeBody(request, chunk); + } + } + } + + private splitByDataChunks(response: HttpResponse): HttpResponse[] { + if (!response.metadata.headers['content-type'].includes('text/event-stream')) { + return [response]; + } + + const text = new TextDecoder().decode(response.raw); + const encoder = new TextEncoder(); + return text + .split('\n') + .filter((line) => line.startsWith('data: ')) + .map((part) => ({ + ...response, + raw: encoder.encode(part), + })); + } + + private decodeBody(request: Request, response: HttpResponse): HttpResponse { if (!this.hasContent(request, response)) { return response; } - if (request.responseContentType === ContentType.Json) { - const decodedBody = new TextDecoder().decode(response.raw); - const json = JSON.parse(decodedBody); - return { - ...response, - data: this.validate(request, json), - }; - } else if ( - request.responseContentType === ContentType.Binary || - request.responseContentType === ContentType.Image + if (request.responseContentType === ContentType.Binary || request.responseContentType === ContentType.Image) { + return this.decodeFile(request, response); + } + + if (request.responseContentType === ContentType.MultipartFormData) { + return this.decodeMultipartFormData(request, response); + } + + if (request.responseContentType === ContentType.Text || request.responseContentType === ContentType.Xml) { + return this.decodeText(request, response); + } + + if (request.responseContentType === ContentType.FormUrlEncoded) { + return this.decodeFormUrlEncoded(request, response); + } + + if ( + request.responseContentType === ContentType.EventStream || + response.metadata.headers['content-type'].includes('text/event-stream') ) { - return { - ...response, - data: this.validate(request, response.raw), - }; - } else if (request.responseContentType === ContentType.Text || request.responseContentType === ContentType.Xml) { - const text = new TextDecoder().decode(response.raw); - return { - ...response, - data: this.validate(request, text), - }; - } else if (request.responseContentType === ContentType.FormUrlEncoded) { - const urlEncoded = this.fromUrlEncoded(new TextDecoder().decode(response.raw)); - return { - ...response, - data: this.validate(request, urlEncoded), - }; - } else if (request.responseContentType === ContentType.MultipartFormData) { - const formData = this.fromFormData(response.raw); - return { - ...response, - data: this.validate(request, formData), - }; - } else { - const decodedBody = new TextDecoder().decode(response.raw); - const json = JSON.parse(decodedBody); - return { - ...response, - data: this.validate(request, json), - }; + return this.decodeEventStream(request, response); + } + + return this.decodeJson(request, response); + } + + private decodeFile(request: Request, response: HttpResponse): HttpResponse { + return { + ...response, + data: this.validate(request, response.raw), + }; + } + + private decodeMultipartFormData(request: Request, response: HttpResponse): HttpResponse { + const formData = this.fromFormData(response.raw); + return { + ...response, + data: this.validate(request, formData), + }; + } + + private decodeText(request: Request, response: HttpResponse): HttpResponse { + const decodedBody = new TextDecoder().decode(response.raw); + return { + ...response, + data: this.validate(request, decodedBody), + }; + } + + private decodeFormUrlEncoded(request: Request, response: HttpResponse): HttpResponse { + const decodedBody = new TextDecoder().decode(response.raw); + const urlEncoded = this.fromUrlEncoded(decodedBody); + return { + ...response, + data: this.validate(request, urlEncoded), + }; + } + + private decodeEventStream(request: Request, response: HttpResponse): HttpResponse { + let decodedBody = new TextDecoder().decode(response.raw); + if (decodedBody.startsWith('data: ')) { + decodedBody = decodedBody.substring(6); } + // Note: this assumes that the content of data is a valid JSON string + const json = JSON.parse(decodedBody); + return { + ...response, + data: this.validate(request, json), + }; + } + + private decodeJson(request: Request, response: HttpResponse): HttpResponse { + const decodedBody = new TextDecoder().decode(response.raw); + const json = JSON.parse(decodedBody); + return { + ...response, + data: this.validate(request, json), + }; } private validate(request: Request, data: any): T { diff --git a/packages/user-client/src/http/handlers/retry-handler.ts b/packages/user-client/src/http/handlers/retry-handler.ts index bae8b6af0..e9ac00351 100644 --- a/packages/user-client/src/http/handlers/retry-handler.ts +++ b/packages/user-client/src/http/handlers/retry-handler.ts @@ -24,6 +24,26 @@ export class RetryHandler implements RequestHandler { throw new Error('Error retrying request.'); } + async *stream(request: Request): AsyncGenerator> { + if (!this.next) { + throw new Error('No next handler set in retry handler.'); + } + + for (let attempt = 1; attempt <= request.retry.attempts; attempt++) { + try { + yield* this.next.stream(request); + return; + } catch (error: any) { + if (!this.shouldRetry(error) || attempt === request.retry.attempts) { + throw error; + } + await this.delay(request.retry.delayMs); + } + } + + throw new Error('Error retrying request.'); + } + private shouldRetry(error: Error): boolean { return error instanceof HttpError && (error.metadata.status >= 500 || error.metadata.status === 408); } diff --git a/packages/user-client/src/http/handlers/terminating-handler.ts b/packages/user-client/src/http/handlers/terminating-handler.ts index 27222b487..43c7b77af 100644 --- a/packages/user-client/src/http/handlers/terminating-handler.ts +++ b/packages/user-client/src/http/handlers/terminating-handler.ts @@ -7,4 +7,8 @@ export class TerminatingHandler implements RequestHandler { async handle(request: Request): Promise> { return new RequestFetchAdapter(request).send(); } + + async *stream(request: Request): AsyncGenerator> { + yield* new RequestFetchAdapter(request).stream(); + } } diff --git a/packages/user-client/src/http/transport/request-fetch-adapter.ts b/packages/user-client/src/http/transport/request-fetch-adapter.ts index f36b89e80..30f04f80e 100644 --- a/packages/user-client/src/http/transport/request-fetch-adapter.ts +++ b/packages/user-client/src/http/transport/request-fetch-adapter.ts @@ -1,9 +1,10 @@ import { HttpError } from '../error.js'; -import { HttpMethod, HttpResponse } from '../types.js'; +import { HttpMetadata, HttpMethod, HttpResponse } from '../types.js'; import { Request } from './request.js'; interface HttpAdapter { send(): Promise; + stream(): AsyncGenerator; } export class RequestFetchAdapter implements HttpAdapter { @@ -19,7 +20,7 @@ export class RequestFetchAdapter implements HttpAdapter { public async send(): Promise> { const response = await fetch(this.request.constructFullUrl(), this.requestInit); - const metadata = { + const metadata: HttpMetadata = { status: response.status, statusText: response.statusText || '', headers: this.getHeaders(response), @@ -31,7 +32,41 @@ export class RequestFetchAdapter implements HttpAdapter { }; } - private setMethod(method: HttpMethod) { + public async *stream(): AsyncGenerator> { + const response = await fetch(this.request.constructFullUrl(), this.requestInit); + + const metadata: HttpMetadata = { + status: response.status, + statusText: response.statusText || '', + headers: this.getHeaders(response), + }; + + if (response.status >= 400) { + throw new HttpError(metadata, await response.clone().arrayBuffer()); + } + + if (!response.body) { + return yield { + metadata, + raw: await response.clone().arrayBuffer(), + }; + } + + const reader = response.body.getReader(); + + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + yield { + metadata, + raw: value, + }; + } + } + + private setMethod(method: HttpMethod): void { if (!method) { return; } @@ -41,7 +76,7 @@ export class RequestFetchAdapter implements HttpAdapter { }; } - private setBody(body: ReadableStream | null) { + private setBody(body: ReadableStream | null): void { if (!body) { return; } @@ -51,7 +86,7 @@ export class RequestFetchAdapter implements HttpAdapter { }; } - private setHeaders(headers: HeadersInit | undefined) { + private setHeaders(headers: HeadersInit | undefined): void { if (!headers) { return; } @@ -62,7 +97,7 @@ export class RequestFetchAdapter implements HttpAdapter { }; } - private setTimeout(timeoutMs: number | undefined) { + private setTimeout(timeoutMs: number | undefined): void { if (!timeoutMs) { return; } diff --git a/packages/user-client/src/http/types.ts b/packages/user-client/src/http/types.ts index b060170d3..fb17dafc7 100644 --- a/packages/user-client/src/http/types.ts +++ b/packages/user-client/src/http/types.ts @@ -30,6 +30,7 @@ export interface RequestHandler { next?: RequestHandler; handle(request: Request): Promise>; + stream(request: Request): AsyncGenerator>; } export enum ContentType { @@ -42,6 +43,7 @@ export enum ContentType { FormUrlEncoded = 'form', Text = 'text', MultipartFormData = 'multipartFormData', + EventStream = 'eventStream', } export interface Options { diff --git a/packages/user-client/src/services/channels/models/apns-token.ts b/packages/user-client/src/services/channels/models/apns-token.ts index 2bffe8b54..b5a4815a5 100644 --- a/packages/user-client/src/services/channels/models/apns-token.ts +++ b/packages/user-client/src/services/channels/models/apns-token.ts @@ -17,9 +17,9 @@ export const apnsToken = z.lazy(() => { /** * * @typedef {ApnsToken} apnsToken + * @property {string} - (Optional) The bundle identifier of the application that is registering this token. Use this field to override the default identifier specified in the projects APNs integration. * @property {string} - * @property {string} - * @property {ApnsTokenInstallationId} + * @property {ApnsTokenInstallationId} - (Optional) The APNs environment the token is registered for. If none is provided we assume the token is used in `production`. */ export type ApnsToken = z.infer; diff --git a/packages/user-client/src/services/channels/models/token-metadata.ts b/packages/user-client/src/services/channels/models/token-metadata.ts index 541df9b14..d1d75bf41 100644 --- a/packages/user-client/src/services/channels/models/token-metadata.ts +++ b/packages/user-client/src/services/channels/models/token-metadata.ts @@ -6,9 +6,9 @@ import { z } from 'zod'; export const tokenMetadata = z.lazy(() => { return z.object({ createdAt: z.string(), - discardedAt: z.string().optional(), + discardedAt: z.string().optional().nullable(), id: z.string(), - updatedAt: z.string().optional(), + updatedAt: z.string().optional().nullable(), }); }); @@ -30,9 +30,9 @@ export const tokenMetadataResponse = z.lazy(() => { return z .object({ created_at: z.string(), - discarded_at: z.string().optional(), + discarded_at: z.string().optional().nullable(), id: z.string(), - updated_at: z.string().optional(), + updated_at: z.string().optional().nullable(), }) .transform((data) => ({ createdAt: data['created_at'], diff --git a/yarn.lock b/yarn.lock index e7d665309..08f76825c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4325,7 +4325,7 @@ resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.4.tgz#cb7e85d9ad5ababfac2f27183e8ac8b576b2abb3" integrity sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw== -"@types/debug@^4", "@types/debug@^4.1.7": +"@types/debug@^4", "@types/debug@^4.0.0", "@types/debug@^4.1.7": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== @@ -4530,6 +4530,13 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.10.tgz#64f3edf656af2fe59e7278b73d3e62404144a6e6" integrity sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ== +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + "@types/mdx@^2.0.0": version "2.0.13" resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" @@ -4774,6 +4781,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + "@types/uuid@^9.0.1": version "9.0.8" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" @@ -6324,6 +6336,11 @@ character-entities@^1.0.0: resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + character-reference-invalid@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" @@ -7117,7 +7134,7 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.6, debug@^4.3.7, debug@^4.4.0: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.6, debug@^4.3.7, debug@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -7150,6 +7167,13 @@ decimal.js@^10.4.2: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -7304,7 +7328,7 @@ dependency-graph@^0.11.0: resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== -dequal@^2.0.2, dequal@^2.0.3: +dequal@^2.0.0, dequal@^2.0.2, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -7344,6 +7368,13 @@ detect-node-es@^1.1.0: resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== +devlop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + devtools-protocol@0.0.1342118: version "0.0.1342118" resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1342118.tgz#ea136fc1701572c0830233dcb414dc857e582e0a" @@ -7929,6 +7960,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + escodegen@^2.0.0, escodegen@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" @@ -8563,6 +8599,13 @@ fault@^1.0.2: dependencies: format "^0.2.0" +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== + dependencies: + format "^0.2.0" + fb-watchman@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -11105,6 +11148,11 @@ log-update@^6.1.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -11252,6 +11300,71 @@ marky@^1.2.2: resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== +mdast-util-from-markdown@^2.0.0, mdast-util-from-markdown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-frontmatter@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz#f5f929eb1eb36c8a7737475c7eb438261f964ee8" + integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + escape-string-regexp "^5.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-markdown@^2.0.0, mdast-util-to-markdown@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + +mdast@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast/-/mdast-3.0.0.tgz#626bce9603ed43fb6fb053245a6e4a17f4457aa8" + integrity sha512-xySmf8g4fPKMeC07jXGz971EkLbWAJ83s4US2Tj9lEdnZ142UP5grN73H1Xd3HzrdbU5o9GYYP/y8F9ZSwLE9g== + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -11299,6 +11412,210 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromark-core-commonmark@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz#6a45bbb139e126b3f8b361a10711ccc7c6e15e93" + integrity sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-frontmatter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz#651c52ffa5d7a8eeed687c513cd869885882d67a" + integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== + dependencies: + fault "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.3.tgz#70ffb99a454bd8c913c8b709c3dc97baefb65f96" + integrity sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.1.tgz#a3edfda3022c6c6b55bfb049ef5b75d70af50709" + integrity sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ== + +micromark@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.1.tgz#294c2f12364759e5f9e925a767ae3dfde72223ff" + integrity sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromatch@^4.0.2, micromatch@^4.0.4, micromatch@~4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -14823,6 +15140,46 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-remove@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-4.0.0.tgz#94b7d6bbd24e42d2f841e947ed087be5c82b222e" + integrity sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e" @@ -15515,6 +15872,11 @@ zustand@^5.0.1: resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.1.tgz#2bdca5e4be172539558ce3974fe783174a48fdcf" integrity sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w== +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== + zx@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/zx/-/zx-8.2.0.tgz#46e8594bf2fe8c6bc15d6e571108e525da3c22b1"