From 2d82ddcddc9c28d5bf83edc407ace3d49686edaa Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 21 Aug 2024 22:49:59 +0800 Subject: [PATCH] feat(route/rss3): RSS3 metadata parser (#16486) * init * done Signed-off-by: Innei * chore: cleanup Signed-off-by: Innei * fix: format Signed-off-by: Innei * fix: pass through Signed-off-by: Innei * chore: add testcase Signed-off-by: Innei * fix: update Signed-off-by: Innei --------- Signed-off-by: Innei --- lib/routes/rss3/index.ts | 460 ++++++++++++++++++++++++- lib/routes/rss3/interfaces/metadata.ts | 67 ++++ lib/utils/camelcase-keys.spec.ts | 127 +++++++ lib/utils/camelcase-keys.ts | 27 ++ package.json | 3 +- pnpm-lock.yaml | 44 +++ 6 files changed, 714 insertions(+), 14 deletions(-) create mode 100644 lib/routes/rss3/interfaces/metadata.ts create mode 100644 lib/utils/camelcase-keys.spec.ts create mode 100644 lib/utils/camelcase-keys.ts diff --git a/lib/routes/rss3/index.ts b/lib/routes/rss3/index.ts index 49ad4ddd8..820e36c2b 100644 --- a/lib/routes/rss3/index.ts +++ b/lib/routes/rss3/index.ts @@ -1,6 +1,10 @@ -import { Route } from '@/types'; +/* eslint-disable default-case */ +import { Route, type DataItem } from '@/types'; +import { camelcaseKeys } from '@/utils/camelcase-keys'; import ofetch from '@/utils/ofetch'; +import type { Action } from '@rss3/sdk'; +import type { GetRSS3DataMetadata } from './interfaces/metadata'; export const route: Route = { path: '/:account/:network?/:tag?', @@ -132,17 +136,447 @@ async function handler(ctx) { return { title: `${account} activities`, link: 'https://rss3.io', - item: data.map((item) => ({ - title: `New ${item.tag} ${item.type} action on ${item.network}`, - description: `New ${item.tag} ${item.type} action on ${item.network}

From: ${item.from}
To: ${item.to}`, - link: item.actions?.[0]?.related_urls?.[0], - guid: item.id, - author: [ - { - name: item.owner, - avatar: `https://cdn.stamp.fyi/avatar/eth:${item.owner}`, - }, - ], - })), + item: data.map((item) => { + const content = parseItemActionToContent(camelcaseKeys(item.actions)); + + const description = `New ${item.tag} ${item.type} action on ${item.network}

From: ${item.from}
To: ${item.to}`; + return { + title: `New ${item.tag} ${item.type} action on ${item.network}`, + description: content ? `${description}

${content}` : description, + link: item.actions?.[0]?.related_urls?.[0], + guid: item.id, + author: [ + { + name: item.owner, + avatar: `https://cdn.stamp.fyi/avatar/eth:${item.owner}`, + }, + ], + + _extra: { raw: item }, + } as DataItem; + }), }; } + +function parseItemActionToContent(actions: Action[]): string | undefined { + if (!actions) { + return; + } + + let joint = ''; + + for (const action of actions) { + const metadata = action.metadata; + if (!metadata) { + continue; + } + const { tag } = action; + switch (tag) { + case 'social': + joint += renderSocialTagContent(action); + break; + case 'collectible': + joint += renderCollectibleTagContent(action); + break; + case 'metaverse': + joint += renderMetaverseTagContent(action); + + break; + case 'exchange': + joint += renderExchange(action); + break; + case 'transaction': + joint += renderTransaction(action); + break; + } + + joint += '
'; + } + + return joint; +} + +const renderTransaction = (action: Action) => { + let joint = ''; + const { type } = action; + const tag = 'transaction'; + switch (type) { + case 'transfer': + case 'burn': + case 'mint': + case 'approval': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Transaction ${type.toUpperCase().at(0) + type.slice(1)}

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

Standard: ${metadata.standard}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Decimals: ${metadata.decimals}

`, + /* html */ `

Address: ${metadata.address}

`, + ]); + break; + } + case 'event': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([/* html */ `

Transaction Event

`, /* html */ `

Block Hash: ${metadata.block.hash}

`, /* html */ `

Transaction Hash: ${metadata.transaction.hash}

`]); + break; + } + case 'bridge': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Transaction Bridge

`, + /* html */ `

Action: ${metadata.action}

`, + /* html */ `

Source Network: ${metadata.sourceNetwork}

`, + /* html */ `

Target Network: ${metadata.targetNetwork}

`, + metadata.token && /* html */ `

Token name: ${metadata.token.name}

`, + metadata.token && /* html */ `

Token Symbol: ${metadata.token.symbol}

`, + metadata.token && /* html */ `

Token Value: ${metadata.token.value}

`, + metadata.token && /* html */ `

Token Address: ${metadata.token.address}

`, + ]); + break; + } + } + + return buildSectionFooterHTML(joint, action); +}; + +const renderExchange = (action: Action) => { + let joint = ''; + const { type } = action; + const tag = 'exchange'; + switch (type) { + case 'liquidity': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Exchange Liquidity

`, + /* html */ `

Action: ${metadata.action}

`, + /* html */ `

+ + + + + + + + + + + + + ${metadata.tokens.map( + (token) => /* html */ ` + + + + + + + ` + )} + +
AddressValueNameSymbolDecimalsStandard
${token.address}${token.value}${token.name}${token.symbol}${token.decimals}${token.standard}
+

`, + ]); + break; + } + case 'staking': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Exchange Liquidity

`, + /* html */ `

Action: ${metadata.action}

`, + metadata.token && + /* html */ `

+ + Token: +

    +
  • Address: ${metadata.token.address}
  • +
  • Value: ${metadata.token.value}
  • +
  • Name: ${metadata.token.name}
  • +
  • Symbol: ${metadata.token.symbol}
  • +
  • Decimals: ${metadata.token.decimals}
  • +
  • Standard: ${metadata.token.standard}
  • +
+

`, + ]); + break; + } + case 'swap': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Exchange Swap

`, + /* html */ metadata.from && `

From: ${metadata.from.address}

`, + /* html */ metadata.to && `

To: ${metadata.to?.address}

`, + ]); + } + } + + return buildSectionFooterHTML(joint, action); +}; + +const renderMetaverseTagContent = (action: Action) => { + let joint = ''; + const { from, to, type } = action; + const tag = 'metaverse'; + switch (type) { + case 'burn': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Metaverse Burn

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + break; + } + case 'trade': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Metaverse Trade

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + break; + } + case 'mint': + { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Metaverse Mint

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + } + break; + case 'transfer': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Metaverse Transfer

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + } + } + + return buildSectionFooterHTML(joint, action); +}; +const renderCollectibleTagContent = (action: Action) => { + let joint = ''; + const { from, to, type } = action; + const tag = 'collectible'; + switch (type) { + case 'approval': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Collectible Approval

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + + break; + } + case 'burn': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Collectible Burn

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + break; + } + case 'trade': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Collectible Trade

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + break; + } + case 'mint': + { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Collectible Mint

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + } + break; + case 'transfer': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html */ `

Collectible Transfer

`, + /* html */ `

Name: ${metadata.name}

`, + /* html */ `

Address: ${metadata.address}

`, + /* html */ `

Symbol: ${metadata.symbol}

`, + /* html */ `

Value: ${metadata.value}

`, + /* html */ `

${from} --> ${to}

`, + ]); + } + } + + return buildSectionFooterHTML(joint, action); +}; + +const renderSocialTagContent = (action: Action) => { + let joint = ''; + const { from, to, platform, type } = action; + const tag = 'social'; + switch (type) { + case 'profile': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + + joint += buildHTML([ + `

Name: ${metadata.name}

`, + `

Handle: ${metadata.handle}

`, + `

Bio: ${metadata.bio}

`, + `

Platform: ${platform}

`, + metadata.imageUri && `${metadata.name}`, + ]); + break; + } + case 'mint': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([`

Social Mint

`, `

Title: ${metadata.title}

`, `

${from} --> ${to}

`]); + break; + } + case 'delete': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([`

Social Delete

`, `

Title: ${metadata.title}

`]); + break; + } + case 'post': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html*/ `

Social Post

`, + /* html*/ `

Title: ${metadata.title}

`, + /* html*/ `

Author: ${metadata.handle}

`, + /* html*/ `

Content:

${metadata.body}

`, + /* html*/ `

Platform: ${platform}

`, + ]); + break; + } + case 'comment': { + const metadata = extractMetadata(tag, type, action); + if (!metadata) { + break; + } + joint += buildHTML([ + /* html*/ `

Social Comment

`, + /* html*/ `

Comment Anchor:${metadata.handle}

`, + metadata.target && /* html*/ `

Comment Target: ${metadata.target.title || metadata.targetUrl}

`, + ]); + + break; + } + case 'reward': + case 'revise': + case 'proxy': + case 'share': + break; + } + + return joint; +}; + +function extractMetadata(_tag: T1, _type: T2, data: any): GetRSS3DataMetadata | null { + const metadata = data.metadata; + if (!metadata) { + return null; + } + return camelcaseKeys(data.metadata) as GetRSS3DataMetadata; +} + +function buildHTML(arr: (string | boolean | undefined | null)[]): string { + return arr.filter(Boolean).join('\n'); +} + +const buildSectionFooterHTML = (string: string, action: Action) => + buildHTML([ + string, + !!action.platform && `

Platform: ${action.platform}

`, + + /* html */ `

Related URLs: +

  • ${action.relatedUrls.map((url) => `${url}`).join('
  • ')}

`, + ]); diff --git a/lib/routes/rss3/interfaces/metadata.ts b/lib/routes/rss3/interfaces/metadata.ts new file mode 100644 index 000000000..52b40a8c3 --- /dev/null +++ b/lib/routes/rss3/interfaces/metadata.ts @@ -0,0 +1,67 @@ +import { + CollectibleApproval, + CollectibleBurn, + CollectibleMint, + CollectibleTrade, + CollectibleTransfer, + ExchangeLiquidity, + ExchangeStaking, + ExchangeSwap, + MetaverseBurn, + MetaverseMint, + MetaverseTrade, + MetaverseTransfer, + SocialComment, + SocialDelete, + SocialMint, + SocialPost, + SocialProfile, + SocialProxy, + SocialRevise, + SocialReward, + SocialShare, + StakeStaking, + StakeTransaction, + StakerProfitSnapshot, + TransactionApproval, + TransactionBridge, + TransactionBurn, + TransactionEvent, + TransactionMint, + TransactionTransfer, +} from '@rss3/sdk'; +export type RSS3DataModels = { + CollectibleApproval: CollectibleApproval; + CollectibleBurn: CollectibleBurn; + CollectibleMint: CollectibleMint; + CollectibleTrade: CollectibleTrade; + CollectibleTransfer: CollectibleTransfer; + MetaverseBurn: MetaverseBurn; + MetaverseMint: MetaverseMint; + MetaverseTrade: MetaverseTrade; + MetaverseTransfer: MetaverseTransfer; + SocialComment: SocialComment; + SocialDelete: SocialDelete; + SocialMint: SocialMint; + SocialPost: SocialPost; + SocialProfile: SocialProfile; + SocialProxy: SocialProxy; + SocialRevise: SocialRevise; + SocialReward: SocialReward; + SocialShare: SocialShare; + StakeStaking: StakeStaking; + StakeTransaction: StakeTransaction; + StakerProfitSnapshot: StakerProfitSnapshot; + TransactionApproval: TransactionApproval; + TransactionBridge: TransactionBridge; + TransactionBurn: TransactionBurn; + TransactionEvent: TransactionEvent; + TransactionMint: TransactionMint; + TransactionTransfer: TransactionTransfer; + ExchangeLiquidity: ExchangeLiquidity; + ExchangeStaking: ExchangeStaking; + ExchangeSwap: ExchangeSwap; +}; +export type GetRSS3DataMetadata = `${Capitalize}${Capitalize}` extends keyof RSS3DataModels + ? RSS3DataModels[`${Capitalize}${Capitalize}`] + : null; diff --git a/lib/utils/camelcase-keys.spec.ts b/lib/utils/camelcase-keys.spec.ts new file mode 100644 index 000000000..af413905d --- /dev/null +++ b/lib/utils/camelcase-keys.spec.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from 'vitest'; + +import { camelcase, camelcaseKeys } from './camelcase-keys'; + +describe('test camelcase keys', () => { + it('case 1 normal', () => { + const obj = { + tool: 'too', + tool_name: 'too_name', + + a_b: 1, + a: 1, + b: { + c_d: 1, + }, + }; + + expect(camelcaseKeys(obj)).toStrictEqual({ + tool: 'too', + toolName: 'too_name', + + aB: 1, + a: 1, + b: { + cD: 1, + }, + }); + }); + + it('case 2: key has number', () => { + const obj = { + b147da0eaecbea00aeb62055: { + data: {}, + }, + a_c11ab_Ac: [ + { + a_b: 1, + }, + 1, + ], + }; + + expect(camelcaseKeys(obj)).toStrictEqual({ + b147da0eaecbea00aeb62055: { + data: {}, + }, + aC11abAc: [ + { + aB: 1, + }, + 1, + ], + }); + }); + + it('case 3: not a object', () => { + const value = 1; + expect(camelcaseKeys(value)).toBe(value); + }); + + it('case 4: nullable value', () => { + let value = null as any; + expect(camelcaseKeys(value)).toBe(value); + + value = undefined; + expect(camelcaseKeys(value)).toBe(value); + + value = Number.NaN; + expect(camelcaseKeys(value)).toBe(value); + }); + + it('case 5: array', () => { + const arr = [ + { + a_b: 1, + }, + null, + undefined, + +0, + -0, + Number.POSITIVE_INFINITY, + { + a_b: 1, + }, + ]; + + expect(camelcaseKeys(arr)).toStrictEqual([ + { + aB: 1, + }, + null, + undefined, + +0, + -0, + Number.POSITIVE_INFINITY, + { + aB: 1, + }, + ]); + }); + + it('case 6: filter out mongo id', () => { + const obj = { + _id: '123', + a_b: 1, + collections: { + posts: { + '661bb93307d35005ba96731b': {}, + }, + }, + }; + + expect(camelcaseKeys(obj)).toStrictEqual({ + id: '123', + aB: 1, + collections: { + posts: { + '661bb93307d35005ba96731b': {}, + }, + }, + }); + }); + + it('case 7: start with underscore should not camelcase', () => { + expect(camelcase('_id')).toBe('id'); + }); +}); diff --git a/lib/utils/camelcase-keys.ts b/lib/utils/camelcase-keys.ts new file mode 100644 index 000000000..9797d698e --- /dev/null +++ b/lib/utils/camelcase-keys.ts @@ -0,0 +1,27 @@ +const isObject = (obj: any) => obj && typeof obj === 'object'; +const isPlainObject = (obj: any) => isObject(obj) && Object.prototype.toString.call(obj) === '[object Object]' && Object.getPrototypeOf(obj) === Object.prototype; + +/** + * A simple camelCase function that only handles strings, but not handling symbol, date, or other complex case. + * If you need to handle more complex cases, please use camelcase-keys package. + */ +export const camelcaseKeys = (obj: any): T => { + if (Array.isArray(obj)) { + return obj.map((x) => camelcaseKeys(x)) as any; + } + + if (isPlainObject(obj)) { + return Object.keys(obj).reduce((result: any, key) => { + const nextKey = isMongoId(key) ? key : camelcase(key); + result[nextKey] = camelcaseKeys(obj[key]); + return result; + }, {}) as any; + } + + return obj; +}; + +export function camelcase(str: string) { + return str.replace(/^_+/, '').replaceAll(/([_-][a-z])/gi, ($1) => $1.toUpperCase().replace('-', '').replace('_', '')); +} +const isMongoId = (id: string) => id.length === 24 && /^[\dA-Fa-f]{24}$/.test(id); diff --git a/package.json b/package.json index f472870c4..b178966dc 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@opentelemetry/sdk-trace-base": "1.25.1", "@opentelemetry/semantic-conventions": "1.25.1", "@postlight/parser": "2.2.3", + "@rss3/sdk": "0.0.13", "@scalar/hono-api-reference": "0.5.141", "@sentry/node": "7.116.0", "@tonyrl/rand-user-agent": "2.0.74", @@ -196,4 +197,4 @@ "engines": { "node": ">=22" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8052e7158..640e95181 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@postlight/parser': specifier: 2.2.3 version: 2.2.3 + '@rss3/sdk': + specifier: 0.0.13 + version: 0.0.13 '@scalar/hono-api-reference': specifier: 0.5.141 version: 0.5.141(hono@4.5.7) @@ -1740,6 +1743,15 @@ packages: cpu: [x64] os: [win32] + '@rss3/api-core@0.0.13': + resolution: {integrity: sha512-5CRVS0FpCZ8YiS/ttHM0ZxPxM05+WkcEPE7a5klX9NvULbDCISdpYQkiUkdVjZw+hPD7MCtlvBc9tFqTQKojJw==} + + '@rss3/api-utils@0.0.13': + resolution: {integrity: sha512-ucN9myX/E0DlA3Q+KkpCD/HwpB6fQSMlaHqRhDDV3tuyYrQnP/PjRKpGibx1Sz2x519SJNnsfyDL931H00iDJA==} + + '@rss3/sdk@0.0.13': + resolution: {integrity: sha512-5QcC9NeMiKeay/H1JEGlG2ZtmKhZxlyLTA5f6dcIdGZUrSNk4DCM/J+8u0ED73bMQupi6xnUm+dIJdn6A1kHyA==} + '@scalar/hono-api-reference@0.5.141': resolution: {integrity: sha512-EFGgjf91RRRlyOJgZfCH/aiD8UXhgIeFdNEdHQZF20UoJr5NvugKUsK3YYm8StkJ3Q7ApXm9ASnkeU2oiGgbnA==} engines: {node: '>=18'} @@ -4445,6 +4457,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + openapi-fetch@0.11.1: + resolution: {integrity: sha512-WtDQsrvxjXuCmo6u6WMQPfUaya8cLfL+ZCaXorPo9MMumqlU/Km/SrCXsEcJH234D4iykOkvJ6Q/iWBzK7+3rA==} + + openapi-typescript-helpers@0.0.12: + resolution: {integrity: sha512-FO+5kTWO6KDutigamr2MRwciYkAUYhqdctlyVRrQOe2uxif2/O2+GcS07jNnP36AUK6ubSsGu3GeBiYIc6eQzA==} + openapi3-ts@4.3.3: resolution: {integrity: sha512-LKkzBGJcZ6wdvkKGMoSvpK+0cbN5Xc3XuYkJskO+vjEQWJgs1kgtyUk0pjf8KwPuysv323Er62F5P17XQl96Qg==} @@ -5408,6 +5426,9 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-case-convert@2.0.7: + resolution: {integrity: sha512-Kqj8wrkuduWsKUOUNRczrkdHCDt4ZNNd6HKjVw42EnMIGHQUABS4pqfy0acETVLwUTppc1fzo/yi11+uMTaqzw==} + ts-custom-error@2.2.2: resolution: {integrity: sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg==} engines: {node: '>=8.0.0'} @@ -7303,6 +7324,21 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.21.0': optional: true + '@rss3/api-core@0.0.13': + dependencies: + openapi-fetch: 0.11.1 + ts-case-convert: 2.0.7 + type-fest: 4.25.0 + + '@rss3/api-utils@0.0.13': + dependencies: + '@rss3/api-core': 0.0.13 + + '@rss3/sdk@0.0.13': + dependencies: + '@rss3/api-core': 0.0.13 + '@rss3/api-utils': 0.0.13 + '@scalar/hono-api-reference@0.5.141(hono@4.5.7)': dependencies: '@scalar/types': 0.0.3 @@ -10402,6 +10438,12 @@ snapshots: dependencies: mimic-function: 5.0.1 + openapi-fetch@0.11.1: + dependencies: + openapi-typescript-helpers: 0.0.12 + + openapi-typescript-helpers@0.0.12: {} + openapi3-ts@4.3.3: dependencies: yaml: 2.5.0 @@ -11443,6 +11485,8 @@ snapshots: dependencies: typescript: 5.5.4 + ts-case-convert@2.0.7: {} + ts-custom-error@2.2.2: {} ts-custom-error@3.3.1: {}